|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- # pubcontrolclient.py
- # ~~~~~~~~~
- # This module implements the PubControlClient class.
- # :authors: Justin Karneges, Konstantin Bokarius.
- # :copyright: (c) 2015 by Fanout, Inc.
- # :license: MIT, see LICENSE for more details.
-
- import sys
- import json
- import copy
- from base64 import b64encode
- import threading
- from collections import deque
- import requests
- from .pubsubmonitor import PubSubMonitor
- from .utilities import _gen_auth_jwt_header
-
- try:
- import ndg.httpsclient
- except ImportError:
- ndg = None
-
- # TODO: Remove in the future when most users have Python >= 2.7.9.
- try:
- import urllib3
- except ImportError:
- try:
- from requests.packages import urllib3
- except ImportError:
- pass
- try:
- urllib3.disable_warnings()
- except NameError:
- pass
- except AttributeError:
- pass
-
- # The PubControlClient class allows consumers to publish either synchronously
- # or asynchronously to an endpoint of their choice. The consumer wraps a Format
- # class instance in an Item class instance and passes that to the publish
- # method. The async publish method has an optional callback parameter that
- # is called after the publishing is complete to notify the consumer of the
- # result. Optionally provide JWT authentication claim and key information.
- # If require_subscribers is set to True then channel subscription monitoring
- # will be enabled and only channels that are subscribed to will be published
- # to.
- class PubControlClient(object):
-
- # Initialize this class with a URL representing the publishing endpoint.
- def __init__(self, uri, auth_jwt_claim=None,
- auth_jwt_key=None, require_subscribers=False, sub_callback=None):
- self.uri = uri
- self.lock = threading.Lock()
- self.thread = None
- self.thread_cond = None
- self.req_queue = deque()
- self.auth_basic_user = None
- self.auth_basic_pass = None
- self.auth_jwt_claim = auth_jwt_claim
- self.auth_jwt_key = auth_jwt_key
- self.requests_session = requests.session()
- self.sub_monitor = None
- self.closed = False
- if require_subscribers:
- self.sub_monitor = PubSubMonitor(uri, auth_jwt_claim, auth_jwt_key, sub_callback)
-
- # Call this method and pass a username and password to use basic
- # authentication with the configured endpoint.
- def set_auth_basic(self, username, password):
- self._verify_notclosed()
- self.lock.acquire()
- self.auth_basic_user = username
- self.auth_basic_pass = password
- self.lock.release()
-
- # Call this method and pass a claim and key to use JWT authentication
- # with the configured endpoint.
- def set_auth_jwt(self, claim, key):
- self._verify_notclosed()
- self.lock.acquire()
- self.auth_jwt_claim = claim
- self.auth_jwt_key = key
- self.lock.release()
-
- # The publish method for publishing the specified item to the specified
- # channel on the configured endpoint. The blocking parameter indicates
- # whether the the callback method should be blocking or non-blocking. The
- # callback parameter is optional and will be passed the publishing results
- # after publishing is complete. Note that the callback executes on a
- # separate thread. If require_subscribers was set to True then the message
- # will only be published if the channel is subscribed to. If the sub_monitor
- # instance failed to retrieve subscriber information then an error will be
- # raised.
- def publish(self, channel, item, blocking=False, callback=None):
- self._verify_notclosed()
- if self.sub_monitor and self.sub_monitor.is_closed():
- if callback:
- callback(False, 'failed to retrieve channel subscribers')
- else:
- raise ValueError('failed to retrieve channel subscribers')
- elif self.sub_monitor and not self.sub_monitor.is_channel_subscribed_to(channel):
- if callback:
- callback(True, '')
- return
- i = item.export()
- i['channel'] = channel
- if blocking:
- self.lock.acquire()
- uri = self.uri
- auth = self._gen_auth_header()
- self.lock.release()
- self._pubcall(uri, auth, [i])
- else:
- self.lock.acquire()
- uri = self.uri
- auth = self._gen_auth_header()
- self._ensure_thread()
- self.lock.release()
- self._queue_req(('pub', uri, auth, i, callback))
-
- # This method is a blocking method that ensures that all asynchronous
- # publishing is complete prior to returning and allowing the consumer to
- # proceed.
- def wait_all_sent(self):
- self.lock.acquire()
- if self.thread is not None:
- self._queue_req(('stop',))
- self.thread.join()
- self.thread = None
- self.lock.release()
-
- # DEPRECATED: The finish method is now deprecated in favor of the more
- # descriptive wait_all_sent() method.
- def finish(self):
- self.wait_all_sent()
-
- # This method is meant to close the PubControlClient instance and should
- # implemented when needed for future features. Currently this method
- # is simply a passthrough to wait_all_sent().
- def close(self):
- self.lock.acquire()
- self.closed = True
- if self.sub_monitor:
- self.sub_monitor.close()
- self.lock.release()
- self.wait_all_sent()
-
- # This method makes an HTTP request to an endpoint relative to the base
- # URI, using configured authentication. Returns a tuple of
- # (status code, headers, body).
- def http_call(self, endpoint, data, headers={}):
- uri = self.uri + endpoint
-
- send_headers = copy.deepcopy(headers)
-
- auth_header = self._gen_auth_header()
- if auth_header:
- send_headers['Authorization'] = auth_header
-
- try:
- return self._make_http_request(uri, data, send_headers)
- except Exception as e:
- raise ValueError('failed during http call: ' + str(e))
-
- # An internal method for verifying that the PubControlClient instance
- # has not been closed via the close() method. If it has then an error
- # is raised.
- def _verify_notclosed(self):
- if self.closed:
- raise ValueError('pubcontrolclient instance is closed')
-
- # An internal method used to generate an authorization header. The
- # authorization header is generated based on whether basic or JWT
- # authorization information was provided via the publicly accessible
- # 'set_*_auth' methods defined above.
- def _gen_auth_header(self):
- if self.auth_basic_user:
- return 'Basic ' + str(b64encode(('%s:%s' % (self.auth_basic_user, self.auth_basic_pass)).encode('ascii')))
- elif self.auth_jwt_claim:
- return _gen_auth_jwt_header(self.auth_jwt_claim, self.auth_jwt_key)
- else:
- return None
-
- # An internal method that ensures that asynchronous publish calls are
- # properly processed. This method initializes the required class fields,
- # starts the pubworker worker thread, and is meant to execute only when
- # the consumer makes an asynchronous publish call.
- def _ensure_thread(self):
- if self.thread is None:
- self.thread_cond = threading.Condition()
- self.thread = threading.Thread(target=self._pubworker)
- self.thread.daemon = True
- self.thread.start()
-
- # An internal method for adding an asynchronous publish request to the
- # publishing queue. This method will also activate the pubworker worker
- # thread to make sure that it process any and all requests added to
- # the queue.
- def _queue_req(self, req):
- self.thread_cond.acquire()
- self.req_queue.append(req)
- self.thread_cond.notify()
- self.thread_cond.release()
-
- # An internal method for preparing the HTTP POST request for publishing
- # data to the endpoint. This method accepts the URI endpoint, authorization
- # header, and a list of items to publish.
- def _pubcall(self, uri, auth_header, items):
- uri = uri + '/publish/'
-
- headers = dict()
- if auth_header:
- headers['Authorization'] = auth_header
- headers['Content-Type'] = 'application/json'
-
- content = dict()
- content['items'] = items
- content_raw = json.dumps(content)
-
- try:
- if isinstance(content_raw, unicode):
- content_raw = content_raw.encode('utf-8')
- except NameError:
- if isinstance(content_raw, str):
- content_raw = content_raw.encode('utf-8')
-
- try:
- self._make_http_request(uri, content_raw, headers)
- except Exception as e:
- raise ValueError('failed to publish: ' + str(e))
-
- # An internal method for making an HTTP request to the specified URI
- # with the specified content and headers.
- def _make_http_request(self, uri, data, headers):
- if sys.version_info >= (2, 7, 9) or (ndg and ndg.httpsclient):
- res = self.requests_session.post(uri, headers=headers, data=data)
- else:
- res = self.requests_session.post(uri, headers=headers, data=data, verify=False)
- self._verify_status_code(res.status_code, res.text)
- return (res.status_code, res.headers, res.text)
-
- # An internal method for ensuring a successful status code is returned
- # from the server.
- def _verify_status_code(self, code, message):
- if code < 200 or code >= 300:
- raise ValueError('received failed status code ' + str(code) +
- ' with message: ' + message)
-
- # An internal method for publishing a batch of requests. The requests are
- # parsed for the URI, authorization header, and each request is published
- # to the endpoint. After all publishing is complete, each callback
- # corresponding to each request is called (if a callback was originally
- # provided for that request) and passed a result indicating whether that
- # request was successfully published.
- def _pubbatch(self, reqs):
- assert(len(reqs) > 0)
- uri = reqs[0][0]
- auth_header = reqs[0][1]
- items = list()
- callbacks = list()
- for req in reqs:
- items.append(req[2])
- callbacks.append(req[3])
-
- try:
- self._pubcall(uri, auth_header, items)
- result = (True, '')
- except Exception as e:
- try:
- result = (False, e.message)
- except AttributeError:
- result = (False, str(e))
-
- for c in callbacks:
- if c:
- c(result[0], result[1])
-
- # An internal method that is meant to run as a separate thread and process
- # asynchronous publishing requests. The method runs continously and
- # publishes requests in batches containing a maximum of 10 requests. The
- # method completes and the thread is terminated only when a 'stop' command
- # is provided in the request queue.
- def _pubworker(self):
- quit = False
- while not quit:
- self.thread_cond.acquire()
-
- # if no requests ready, wait for one
- if len(self.req_queue) == 0:
- self.thread_cond.wait()
-
- # still no requests after notification? start over
- if len(self.req_queue) == 0:
- self.thread_cond.release()
- continue
-
- reqs = list()
- while len(self.req_queue) > 0 and len(reqs) < 10:
- m = self.req_queue.popleft()
- if m[0] == 'stop':
- quit = True
- break
- reqs.append((m[1], m[2], m[3], m[4]))
-
- self.thread_cond.release()
-
- if len(reqs) > 0:
- self._pubbatch(reqs)
|