def broadcast(cls, channels, trigger=None, originating_client=None): """ Trigger all subscriptions on the given channel(s). This method is called in the "broadcaster" thread, which means that all subscription updates happen in the same thread. Callers can pass an "originating_client" id, which will prevent data from being pushed to those clients. This is useful in cases like this: -> a Javascipt application makes a call like "ecard.delete" -> not wanting to wait for a subscription update, the Javascript app preemptively updates its local data store to remove the item -> the response to the delete call comes back as a success -> because the local data store was already updated, there's no need for this client to get a subscription update Callers can pass a "trigger" field, which will be included in the subscription update message as the reason for the update. This doesn't affect anything, but might be useful for logging. """ triggered = set() for channel in sideboard.lib.listify(channels): for websocket, clients in list(cls.subscriptions[channel].items()): if websocket.is_closed: websocket.unsubscribe_all() else: for client, callbacks in clients.copy().items(): if client != originating_client: for callback in callbacks: triggered.add((websocket, client, callback)) for websocket, client, callback in triggered: try: websocket.trigger(client=client, callback=callback, trigger=trigger) except: log.warn('ignoring unexpected trigger error', exc_info=True)
def _run_shutdown(): for priority, functions in sorted(_shutdown_registry.items()): for func in functions: try: func() except Exception: log.warn('Ignored exception during shutdown', exc_info=True)
def subscribe(self, callback, method, *args, **kwargs): """ Send a websocket request which you expect to subscribe you to a channel with a callback which will be called every time there is new data, and return the client id which uniquely identifies this subscription. Callback may be either a function or a dictionary in the form { 'callback': <function>, 'errback': <function>, # optional 'paramback: <function>, # optional 'client': <string> # optional } Both callback and errback take a single argument; for callback, this is the return value of the method, for errback it is the error message returning. If no errback is specified, we will log errors at the ERROR level and do nothing further. The paramback function exists for subscriptions where we might want to pass different parameters every time we reconnect. This might be used for e.g. time-based parameters. This function takes no arguments and returns the parameters which should be passed every time we connect and fire (or re-fire) all of our subscriptions. The client id is automatically generated if omitted, and you should not set this yourself unless you really know what you're doing. The positional and keyword arguments passed to this function will be used as the arguments to the remote method, unless paramback is passed, in which case that will be used to generate the params, and args/kwargs will be ignored. """ client = self._next_id('client') if isinstance(callback, Mapping): assert 'callback' in callback, 'callback is required' client = callback.setdefault('client', client) self._callbacks[client] = callback else: self._callbacks[client] = {'client': client, 'callback': callback} paramback = self._callbacks[client].get('paramback') params = self.preprocess( method, paramback() if paramback else (args or kwargs)) self._callbacks[client].setdefault( 'errback', lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result)) self._callbacks[client].update({'method': method, 'params': params}) try: self._send(method=method, params=params, client=client) except: log.warn( 'initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url) return client
def subscribe(self, callback, method, *args, **kwargs): """ Send a websocket request which you expect to subscribe you to a channel with a callback which will be called every time there is new data, and return the client id which uniquely identifies this subscription. Callback may be either a function or a dictionary in the form { 'callback': <function>, 'errback': <function>, # optional 'paramback: <function>, # optional 'client': <string> # optional } Both callback and errback take a single argument; for callback, this is the return value of the method, for errback it is the error message returning. If no errback is specified, we will log errors at the ERROR level and do nothing further. The paramback function exists for subscriptions where we might want to pass different parameters every time we reconnect. This might be used for e.g. time-based parameters. This function takes no arguments and returns the parameters which should be passed every time we connect and fire (or re-fire) all of our subscriptions. The client id is automatically generated if omitted, and you should not set this yourself unless you really know what you're doing. The positional and keyword arguments passed to this function will be used as the arguments to the remote method, unless paramback is passed, in which case that will be used to generate the params, and args/kwargs will be ignored. """ client = self._next_id('client') if isinstance(callback, Mapping): assert 'callback' in callback, 'callback is required' client = callback.setdefault('client', client) self._callbacks[client] = callback else: self._callbacks[client] = { 'client': client, 'callback': callback } paramback = self._callbacks[client].get('paramback') params = self.preprocess(method, paramback() if paramback else (args or kwargs)) self._callbacks[client].setdefault('errback', lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result)) self._callbacks[client].update({ 'method': method, 'params': params }) try: self._send(method=method, params=params, client=client) except: log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url) return client
def _send(self, **kwargs): log.debug('sending {}', kwargs) with self._lock: assert self.connected, 'tried to send data on closed websocket {!r}'.format(self.url) try: return self.ws.send(kwargs) except: log.warn('failed to send {!r} on {!r}, closing websocket and will attempt to reconnect', kwargs, self.url) self.ws.close() raise
def error(code, message): body = { 'jsonrpc': '2.0', 'id': id, 'error': { 'code': code, 'message': message } } log.warn('returning error message: {!r}', body) return body
def internal_action(self, action, client, callback): """ Sideboard currently supports both method calls and "internal actions" which affect the state of the websocket connection itself. This implements the command-dispatch pattern to perform the given action and raises an exception if that action doesn't exist. The only action currently implemented is "unsubscribe". """ if action == 'unsubscribe': self.unsubscribe(client) elif action is not None: log.warn('unknown action {!r}', action)
def _reconnect(self): with self._lock: assert not self.connected, 'connection is still active' try: self.ws = self.WebSocketDispatcher(self._dispatcher, self.url, ssl_opts=self.ssl_opts) self.ws.connect() except Exception as e: log.warn('failed to connect to {}: {}', self.url, str(e)) self._last_reconnect_attempt = datetime.now() self._reconnect_attempts += 1 else: self._reconnect_attempts = 0 self._refire_subscriptions()
def refresh(self): """ Sometimes we want to manually re-fire all of our subscription methods to get the latest data. This is useful in cases where the remote server isn't necessarily programmed to always push the latest data as soon as it's available, usually for performance reasons. This method allows the client to get the latest data more often than the server is programmed to provide it. """ for ws in self.websockets.values(): try: self._callback(self.ws.call(self.method, *self.args, **self.kwargs), ws) except: log.warn('failed to fetch latest data from {} on {}', self.method, ws.url)
def broadcast(cls, channels, trigger=None, originating_client=None): triggered = set() for channel in sideboard.lib.listify(channels): for websocket, clients in cls.subscriptions[channel].items(): for client, callbacks in clients.copy().items(): if client != originating_client: for callback in callbacks: triggered.add((websocket, client, callback)) for websocket, client, callback in triggered: try: websocket.trigger(client=client, callback=callback, trigger=trigger) except: log.warn('ignoring unexpected trigger error', exc_info=True)
def refresh(self): """ Sometimes we want to manually re-fire all of our subscription methods to get the latest data. This is useful in cases where the remote server isn't necessarily programmed to always push the latest data as soon as it's available, usually for performance reasons. This method allows the client to get the latest data more often than the server is programmed to provide it. """ for ws in self.websockets.values(): try: self._callback( self.ws.call(self.method, *self.args, **self.kwargs), ws) except: log.warn('failed to fetch latest data from {} on {}', self.method, ws.url)
def _reconnect(self): with self._lock: assert not self.connected, 'connection is still active' try: self.ws = self.WebSocketDispatcher(self._dispatcher, self.url) self.ws.connect() except Exception as e: log.warn('failed to connect to {}: {}', self.url, str(e)) self._last_reconnect_attempt = datetime.now() self._reconnect_attempts += 1 else: self._reconnect_attempts = 0 try: for cb in self._callbacks.values(): if 'client' in cb: self._send(method=cb['method'], params=cb['params'], client=cb['client']) except: pass # self.send() already closes and logs on error
def connect(self, max_wait=0): """ Start the background threads which connect this websocket and handle RPC dispatching. This method is safe to call even if the websocket is already connected. You may optionally pass a max_wait parameter if you want to wait for up to that amount of time for the connection to go through; if that amount of time elapses without successfully connecting, a warning message is logged. """ self._checker.start() self._dispatcher.start() for i in range(10 * max_wait): if not self.connected: stopped.wait(0.1) else: break else: if max_wait: log.warn('websocket {!r} not connected after {} seconds', self.url, max_wait)
def subscribe(self, callback, method, *args, **kwargs): """ Send a websocket request which you expect to subscribe you to a channel with a callback which will be called every time there is new data, and return the client id which uniquely identifies this subscription. Callback may be either a function or a dictionary in the form { 'callback': <function>, 'errback': <function> } Both callback and errback take a single argument; for callback, this is the return value of the method, for errback it is the error message returning. If no errback is specified, we will log errors at the ERROR level and do nothing further. The positional and keyword arguments passed to this function will be used as the arguments to the remote method. """ client = self._next_id('client') if isinstance(callback, Mapping): assert 'callback' in callback and 'errback' in callback, 'callback and errback are required' client = callback.setdefault('client', client) self._callbacks[client] = callback else: self._callbacks[client] = { 'client': client, 'callback': callback, 'errback': lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result) } self._callbacks[client].update({ 'method': method, 'params': args or kwargs }) try: self._send(method=method, params=args or kwargs, client=client) except: log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url) return client
def subscribe(self, callback, method, *args, **kwargs): """ Send a websocket request which you expect to subscribe you to a channel with a callback which will be called every time there is new data, and return the client id which uniquely identifies this subscription. Callback may be either a function or a dictionary in the form { 'callback': <function>, 'errback': <function> } Both callback and errback take a single argument; for callback, this is the return value of the method, for errback it is the error message returning. If no errback is specified, we will log errors at the ERROR level and do nothing further. The positional and keyword arguments passed to this function will be used as the arguments to the remote method. """ client = self._next_id('client') if isinstance(callback, dict): assert 'callback' in callback and 'errback' in callback, 'callback and errback are required' client = callback.setdefault('client', client) self._callbacks[client] = callback else: self._callbacks[client] = { 'client': client, 'callback': callback, 'errback': lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result) } self._callbacks[client].update({ 'method': method, 'params': args or kwargs }) try: self._send(method=method, params=args or kwargs, client=client) except: log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url) return client
def internal_action(self, action, client, callback): if action == 'unsubscribe': self.unsubscribe(client) elif action is not None: log.warn('unknown action {!r}', action)
def error(code, message): body = {"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}} log.warn("returning error message: {!r}", body) return body
def error(code, message): body = {'jsonrpc': '2.0', 'id': id, 'error': {'code': code, 'message': message}} log.warn('returning error message: {!r}', body) return body