예제 #1
0
파일: client.py 프로젝트: alphatb/domogik
class MqAsyncReq(object):

    """Class for the MDP client side.

    Thin asynchronous encapsulation of a zmq.REQ socket.
    Provides a :func:`request` method with optional timeout.

    Objects of this class are ment to be integrated into the
    asynchronous IOLoop of pyzmq.

    :param context:  the ZeroMQ context to create the socket in.
    :type context:   zmq.Context
    :param endpoint: the enpoint to connect to.
    :type endpoint:  str
    :param service:  the service the client should use
    :type service:   str
    """

    _proto_version = b'MDPC01'

    def __init__(self, context, service):
        """Initialize the MDPClient.
        """
        if ("domogik.common.configloader" in sys.modules):
            cfg = Loader('mq').load()
            confi = dict(cfg[1])
            self.endpoint = "tcp://{0}:{1}".format(config['ip'], config['req_rep_port'])
        else:
            ip = Parameter.objects.get(key='mq-ip')
            port = Parameter.objects.get(key='mq-req_rep_port')
            self.endpoint = "tcp://{0}:{1}".format(ip.value, port.value)
        socket = ZmqSocket(context, zmq.REQ)
        ioloop = IOLoop.instance()
        self.service = service
        self.stream = ZMQStream(socket, ioloop)
        self.stream.on_recv(self._on_message)
        self.can_send = True
        self._proto_prefix = [ PROTO_VERSION, service]
        self._tmo = None
        self.timed_out = False
        socket.connect(self.endpoint)
        return

    def shutdown(self):
        """Method to deactivate the client connection completely.

        Will delete the stream and the underlying socket.

        .. warning:: The instance MUST not be used after :func:`shutdown` has been called.

        :rtype: None
        """
        if not self.stream:
            return
        self.stream.socket.setsockopt(zmq.LINGER, 0)
        self.stream.socket.close()
        self.stream.close()
        self.stream = None
        return

    def request(self, msg, timeout=None):
        """Send the given message.

        :param msg:     message parts to send.
        :type msg:      list of str
        :param timeout: time to wait in milliseconds.
        :type timeout:  int
        
        :rtype None:
        """
        if not self.can_send:
            raise InvalidStateError()
        if type(msg) in (bytes, str):
            msg = [msg]
        # prepare full message
        to_send = self._proto_prefix[:]
        to_send.extend(msg)
        self.stream.send_multipart(to_send)
        self.can_send = False
        if timeout:
            self._start_timeout(timeout)
        return

    def _on_timeout(self):
        """Helper called after timeout.
        """
        self.timed_out = True
        self._tmo = None
        self.on_timeout()
        return

    def _start_timeout(self, timeout):
        """Helper for starting the timeout.

        :param timeout:  the time to wait in milliseconds.
        :type timeout:   int
        """
        self._tmo = DelayedCallback(self._on_timeout, timeout)
        self._tmo.start()
        return

    def _on_message(self, msg):
        """Helper method called on message receive.

        :param msg:   list of message parts.
        :type msg:    list of str
        """
        if self._tmo:
            # disable timout
            self._tmo.stop()
            self._tmo = None
        # setting state before invoking on_message, so we can request from there
        self.can_send = True
        self.on_message(msg)
        return

    def on_message(self, msg):
        """Public method called when a message arrived.

        .. note:: Does nothing. Should be overloaded!
        """
        pass

    def on_timeout(self):
        """Public method called when a timeout occured.

        .. note:: Does nothing. Should be overloaded!
        """
        pass
예제 #2
0
class MDPClient(object):

    """Class for the MDP client side.

    Thin asynchronous encapsulation of a zmq.REQ socket.
    Provides a :func:`request` method with optional timeout.

    Objects of this class are ment to be integrated into the
    asynchronous IOLoop of pyzmq.

    :param context:  the ZeroMQ context to create the socket in.
    :type context:   zmq.Context
    :param endpoint: the enpoint to connect to.
    :type endpoint:  str
    :param service:  the service the client should use
    :type service:   str
    """

    _proto_version = C_CLIENT

    def __init__(self, context, endpoint):
        """Initialize the MDPClient.
        """
        
        self.context = context
        self.endpoint = endpoint
        
    def start(self):
        """
        Initialize the zmq sockets on a ioloop stream.
        The separation of this part from the init is useful if
        we start the client on a separate thread with a new ioloop
        (for example to enable use in an ipython notebook)
        """
        socket = self.context.socket(zmq.DEALER)
        ioloop = IOLoop.instance()
        self.stream = ZMQStream(socket, ioloop)
        self.stream.on_recv(self._on_message)
        self._proto_prefix = [EMPTY_FRAME, self._proto_version]
        self._delayed_timeout = None
        self.timed_out = False
        socket.connect(self.endpoint)

    def shutdown(self):
        """Method to deactivate the client connection completely.

        Will delete the stream and the underlying socket.

        .. warning:: The instance MUST not be used after :func:`shutdown` has been called.

        :rtype: None
        """
        
        if not self.stream:
            return
        
        self.stream.socket.setsockopt(zmq.LINGER, 0)
        self.stream.socket.close()
        self.stream.close()
        self.stream = None

    def request(self, service, msg, msg_extra=STANDARD, timeout=None):
        """Send the given message.

        :param msg:     message parts to send.
        :type msg:      list of str
        :param msg_extra: Extra message flags (e.g. STANDARD or BROADCAST)
        :type msg_extra: int
        :param timeout: time to wait in milliseconds.
        :type timeout:  int
        
        :rtype None:
        """
        if type(msg) in (bytes, unicode):
            msg = [msg]
            
        #
        # prepare full message
        #
        to_send = [chr(msg_extra)] + self._proto_prefix[:]
        to_send.extend([service])
        to_send.extend(msg)
        
        self.stream.send_multipart(to_send)
        
        if timeout:
            self._start_timeout(timeout)

    def _on_timeout(self):
        """Helper called after timeout.
        """
        
        self.timed_out = True
        self._delayed_timeout = None
        self.on_timeout()

    def _start_timeout(self, timeout):
        """Helper for starting the timeout.

        :param timeout:  the time to wait in milliseconds.
        :type timeout:   int
        """
        
        self._delayed_timeout = DelayedCallback(self._on_timeout, timeout)
        self._delayed_timeout.start()

    def _on_message(self, msg):
        """Helper method called on message receive.

        :param msg:   list of message parts.
        :type msg:    list of str
        """
        
        if self._delayed_timeout:
            # 
            # disable timout
            #
            self._delayed_timeout.stop()
            self._delayed_timeout = None
            
        self.on_message(msg)

    def on_message(self, msg):
        """Public method called when a message arrived.

        .. note:: Does nothing. Should be overloaded!
        """

        raise NotImplementedError('on_message must be implemented by the subclass.')
        
    def on_timeout(self):
        """Public method called when a timeout occured.

        .. note:: Does nothing. Should be overloaded!
        """
        raise NotImplementedError('on_timeout must be implemented by the subclass.')
예제 #3
0
파일: client.py 프로젝트: MagnetonBora/zmq
class MqAsyncReq(object):

    """Class for the MDP client side.

    Thin asynchronous encapsulation of a zmq.REQ socket.
    Provides a :func:`request` method with optional timeout.

    Objects of this class are ment to be integrated into the
    asynchronous IOLoop of pyzmq.

    :param context:  the ZeroMQ context to create the socket in.
    :type context:   zmq.Context
    :param endpoint: the enpoint to connect to.
    :type endpoint:  str
    :param service:  the service the client should use
    :type service:   str
    """

    _proto_version = b'MDPC01'

    def __init__(self, context, service):
        """Initialize the MDPClient.
        """
        if ("domogik.common.configloader" in sys.modules):
            cfg = Loader('mq').load()
            confi = dict(cfg[1])
            self.endpoint = "tcp://{0}:{1}".format(config['ip'], config['req_rep_port'])
        else:
            ip = Parameter.objects.get(key='mq-ip')
            port = Parameter.objects.get(key='mq-req_rep_port')
            self.endpoint = "tcp://{0}:{1}".format(ip.value, port.value)
        socket = ZmqSocket(context, zmq.REQ)
        ioloop = IOLoop.instance()
        self.service = service
        self.stream = ZMQStream(socket, ioloop)
        self.stream.on_recv(self._on_message)
        self.can_send = True
        self._proto_prefix = [ PROTO_VERSION, service]
        self._tmo = None
        self.timed_out = False
        socket.connect(self.endpoint)
        return

    def shutdown(self):
        """Method to deactivate the client connection completely.

        Will delete the stream and the underlying socket.

        .. warning:: The instance MUST not be used after :func:`shutdown` has been called.

        :rtype: None
        """
        if not self.stream:
            return
        self.stream.socket.setsockopt(zmq.LINGER, 0)
        self.stream.socket.close()
        self.stream.close()
        self.stream = None
        return

    def request(self, msg, timeout=None):
        """Send the given message.

        :param msg:     message parts to send.
        :type msg:      list of str
        :param timeout: time to wait in milliseconds.
        :type timeout:  int
        
        :rtype None:
        """
        if not self.can_send:
            raise InvalidStateError()
        if type(msg) in (bytes, str):
            msg = [msg]
        # prepare full message
        to_send = self._proto_prefix[:]
        to_send.extend(msg)
        self.stream.send_multipart(to_send)
        self.can_send = False
        if timeout:
            self._start_timeout(timeout)
        return

    def _on_timeout(self):
        """Helper called after timeout.
        """
        self.timed_out = True
        self._tmo = None
        self.on_timeout()
        return

    def _start_timeout(self, timeout):
        """Helper for starting the timeout.

        :param timeout:  the time to wait in milliseconds.
        :type timeout:   int
        """
        self._tmo = DelayedCallback(self._on_timeout, timeout)
        self._tmo.start()
        return

    def _on_message(self, msg):
        """Helper method called on message receive.

        :param msg:   list of message parts.
        :type msg:    list of str
        """
        if self._tmo:
            # disable timout
            self._tmo.stop()
            self._tmo = None
        # setting state before invoking on_message, so we can request from there
        self.can_send = True
        self.on_message(msg)
        return

    def on_message(self, msg):
        """Public method called when a message arrived.

        .. note:: Does nothing. Should be overloaded!
        """
        pass

    def on_timeout(self):
        """Public method called when a timeout occured.

        .. note:: Does nothing. Should be overloaded!
        """
        pass
예제 #4
0
class Client(object):

    def __init__(self, context, endpoint, service):
        """Initialize the Client.
        """
        socket = context.socket(zmq.DEALER)
        self.ioloop = IOLoop.instance()
        self.service = service.encode('utf-8')
        self.endpoint = endpoint
        self.stream = ZMQStream(socket, self.ioloop)
        self.stream.on_recv(self._on_message)
        self.can_send = True
        self._proto_prefix = [MDP_CLIENT_VERSION, b'\x01', service.encode('utf-8')]
        self._tmo = None
        self.timed_out = False
        self.multicast_services = None
        self.multicast_already_received = []
        socket.connect(endpoint)
        return

    def shutdown(self):
        """Method to deactivate the client connection completely.

        Will delete the stream and the underlying socket.

        .. warning:: The instance MUST not be used after :func:`shutdown` has been called.

        :rtype: None
        """
        if not self.stream:
            return
        self.stream.socket.setsockopt(zmq.LINGER, 0)
        self.stream.socket.close()
        self.stream.close()
        self.stream = None
        return

    def request(self, msg, timeout=None):
        """Send the given message.

        :param msg:     message parts to send.
        :type msg:      list of str
        :param timeout: time to wait in milliseconds.
        :type timeout:  int | float

        :rtype None:
        """
        if not self.can_send:
            raise InvalidStateError()
        try:
            if type(msg) in (bytes, unicode):  # python 2
                msg = [msg]
        except NameError:
            if type(msg) == bytes:  # python 3
                msg = [msg]
        # prepare full message
        to_send = [b'']
        to_send.extend(self._proto_prefix[:])
        to_send.extend(msg)
        self.stream.send_multipart(to_send)
        self.can_send = False
        self.multicast_services = None
        if timeout:
            self._start_timeout(timeout)
        return

    def rpc(self, method, args=None, kwargs=None, timeout=None):
        """Call RPC method

        :param method:  name of method
        :type method:   str
        :param args:    list of args
        :type args:     list
        :param kwargs:  list of kwargs
        :type kwargs:   dict
        :param timeout: time to wait in milliseconds.
        :type timeout:  int | float

        :rtype None:
        """
        data = [method.encode('utf-8'), msgpack.packb([] if args is None else args), msgpack.packb({} if kwargs is None else kwargs)]
        return self.request(data, timeout)

    def _on_message(self, message):
        """Helper method called on message receive.

        :param message:   list of message parts.
        :type message:    list of str
        """
        message.pop(0) # remove empty string
        protocol_version = message.pop(0)
        if protocol_version != MDP_WORKER_VERSION:  # version check, ignore old versions
            return
        message_type = message.pop(0)

        if message_type == b'\x02':  # partial message
            worker = message.pop(0)
            try:
                msg = msgpack.unpackb(message[0])
            except:
                msg = message[0]

            self.on_partial_message(worker, msg)
        elif message_type == b'\x03':  # final message
            worker = message.pop(0)
            try:
                msg = msgpack.unpackb(message[0])
            except:
                msg = message[0]
            self.on_message(worker, msg)

            if message[0] not in self.multicast_already_received:
                self.multicast_already_received.append(worker)
            if self.multicast_services is None or len(self.multicast_already_received) == len(self.multicast_services):
                self.stop_waiting()
        elif message_type == b'\x04':  # error message
            worker = message.pop(0)
            self.on_error_message(worker, message[0])
            self.stop_waiting()
        elif message_type == b'\x05':  # multicast start, load list of multicast services
            self.multicast_services = message
            self.multicast_already_received = []
            self.on_multicast_start(message)
        elif message_type == b'\x06':  # exception message
            worker = message.pop(0)
            try:
                msg = msgpack.unpackb(message[0])
            except:
                msg = {b'class': 'Exception', b'message': 'parsing failed', b'traceback': message[0]}
            self.on_exception_message(worker, msg[b'class'], msg[b'message'], msg[b'traceback'])

            if message[0] not in self.multicast_already_received:
                self.multicast_already_received.append(worker)
            if self.multicast_services is None or len(self.multicast_already_received) == len(self.multicast_services):
                self.stop_waiting()
        #else: unknown type - do nothing
        return

    def wait_for_reply(self):
        """Start waiting for replies
        """
        self.ioloop.start()

    def stop_waiting(self):
        """Stop waiting for replies and cancel timeout
        """
        self.ioloop.stop()
        self.can_send = True
        self._stop_timeout()

    def _on_timeout(self):
        """Helper called after timeout.
        """
        self.timed_out = True
        self._tmo = None
        self.ioloop.stop()
        self.on_timeout()
        return

    def _start_timeout(self, timeout):
        """Helper for starting the timeout.

        :param timeout:  the time to wait in milliseconds.
        :type timeout:   int
        """
        self._tmo = DelayedCallback(self._on_timeout, timeout)
        self._tmo.start()
        return

    def _stop_timeout(self):
        """Helper for stopping timeout event
        """
        if self._tmo:
            self._tmo.stop()
            self._tmo = None

    def on_message(self, service, msg):
        """Public method called when a message arrived.

        .. note:: Does nothing. Should be overloaded!

        :param service:  name of the worker
        :type service:   str
        :param msg:      message
        :type msg:       mixed
        """
        pass

    def on_partial_message(self, service, msg):
        """Public method called when a partial message arrived.

        .. note:: Does nothing. Should be overloaded!

        :param service:  name of the worker
        :type service:   str
        :param msg:      message
        :type msg:       mixed
        """
        pass

    def on_error_message(self, worker, msg):
        """Public method called when an error message arrived.

        .. note:: Does nothing. Should be overloaded!

        :param service:  name of the worker
        :type service:   str
        :param msg:      message
        :type msg:       str
        """
        pass

    def on_exception_message(self, service, cls, message, traceback):
        """Public method called when an exception arrived.

        .. note:: Does nothing. Should be overloaded!

        :param service:  name of the worker
        :type service:   str
        :param cls:      exception class name
        :type cls:       str
        :param message:  error message
        :type message:   str
        :param traceback: traceback
        :type traceback:  str
        """
        pass

    def on_timeout(self):
        """Public method called when a timeout occured.

        .. note:: Does nothing. Should be overloaded!
        """
        pass

    def on_multicast_start(self, services):
        """Public method called when multicast request started

        .. note:: Does nothing. Should be overloaded!

        :param services:  list of services in multicast group
        :type services:   list
        """
        pass
예제 #5
0
class DeviceConnection(object):

    SERVICE_NAME = 'device'  # service to connect to
    TIMEOUT = 5  # time to wait for answer in seconds

    # Number of connections to try before restarting when in incorrect state
    CONNECTION_ATTEMPS = 3
    HB_INTERVAL = 1000 * 10  # in milliseconds

    def __init__(self, context, device_id, address):

        self.context = context
        self.device_id = device_id
        self.address = address

        self.ticker = None
        self.updater = None
        self.socket = None
        self.can_send = False

        self._restart()
        self.heartbeat()

        return

    def _restart(self):

        self.shutdown()

        self.socket = self.context.socket(zmq.REQ)
        self.socket.setsockopt(zmq.LINGER, 0)
        self.socket.connect(self.address)

        self.can_send = True
        self.connection_attempts = 0
        self.device_state = 'unknown'
        self.ticker = PeriodicCallback(self.heartbeat,
                                       DeviceConnection.HB_INTERVAL)
        self.ticker.start()

        # delay = seconds_till_next('hour', duration=1) + 2  # 2 second buffer
        delay = 60  # send update one minute after startup
        self.updater = DelayedCallback(self.update, delay * 1000)
        self.updater.start()
        return

    def shutdown(self):

        if self.ticker:
            self.ticker.stop()
            self.ticker = None

        if self.updater:
            self.updater.stop()

        if self.socket:
            self.socket.close()
            self.socket = None

        self.can_send = False
        return

    def send(self, message):

        if not self.can_send:
            self.connection_attempts += 1
            logger.error("DeviceConnection is not in state to send")
            return None
        else:
            self.connection_attempts = 0

        self.can_send = False

        logger.debug("DeviceConnection sending {0}".format(message))

        reply = mdp_request(self.socket,
                            DeviceConnection.SERVICE_NAME,
                            message,
                            DeviceConnection.TIMEOUT)

        if reply:
            logger.debug("DeviceConnection reply received: {0}".format(reply))
            self.can_send = True
            return reply
        else:
            # Timeout! Will be restarted at next heartbeat
            logger.warn("DeviceConnection timeout. Will be restarted at next heartbeat")
            self.connection_attempts = DeviceConnection.CONNECTION_ATTEMPS
            return None

    def heartbeat(self):

        if self.connection_attempts >= DeviceConnection.CONNECTION_ATTEMPS:
            logger.warn("DeviceConnection attempts max reached. Restarting connection.")
            self._restart()

        message = HeartbeatMessage(self.device_id)
        reply = self.send([pickle.dumps(message)])

        if reply:
            message = pickle.loads(reply[1])

            if message.response == self.device_state:
                # device state has not changed
                pass
            elif message.response == 'unknown':
                logger.info("connection status set to 'unknown'")
                self.device_state = 'unknown'
            elif message.response == 'joined':
                logger.info("connection status set to 'joined'")
                self.device_state = 'joined'
            elif message.response == 'connected':
                logger.info("connection status set to 'connected'")
                self.device_state = 'connected'
            else:
                self._restart()
        return

    def create(self):

        logger.info("Sending create details for device")
        device_details = DeviceInfo(self.device_id, 'farm_monitor')
        device_details.create()

        message = [pickle.dumps(device_details)]
        reply = self.send(message)

        if reply:
            message = pickle.loads(reply[1])
            logger.debug("Update response from {0} :{1}".format(message.source, message.reply))

        return

    def update(self):

        self.updater.stop()

        if self.device_state == 'connected':
            logger.info("Sending update for device")
            update = DeviceUpdate(self.device_id, 'farm_monitor')
            update.create()
            reply = self.send([pickle.dumps(update)])

            if reply:
                message = pickle.loads(reply[1])
                logger.debug("Update response from {0} :{1}".format(message.source, message.reply))

        # delay = seconds_till_next('hour', duration=1) + 2  # 2 second buffer
        delay = 60 * 60  # send next update in 60 minutes
        self.updater = DelayedCallback(self.update, delay * 1000)
        self.updater.start()

        return

    def backup(self):

        backup_index = create_backup()

        session = Session()
        backup = session.query(Backup).filter_by(index=backup_index).first()

        standalone_configuration = session.query(SystemSetup.standalone_configuration).scalar()

        if self.device_state == 'connected':

            failed_attempts = 0

            # don't send the file if we are a combined configuration
            if standalone_configuration:
                logger.info("Sending device backup file to FarmMonitor")
                file_size = os.path.getsize(backup.filepath)
                bytes_sent = 0
                zip_file = open(backup.filepath, 'r')

                while bytes_sent < file_size and failed_attempts < 3:
                    zip_file.seek(bytes_sent)
                    data = zip_file.read(512)
                    logger.debug("Sending {0} bytes of backup file".format(len(data)))
                    message = FileMessage(self.device_id, 'farm_monitor', 'backup')
                    message.set_file(backup.filepath, data)
                    reply = self.send([pickle.dumps(message)])

                    if reply:
                        # only mark the data received if a reply is recieved
                        bytes_sent += len(data)
                        failed_attempts = 0
                        reply_message = pickle.loads(reply[1])
                        logger.debug("Update reply: {0}".format(reply_message.reply))
                    else:
                        logger.warn("Failed attempt in sending backup file")
                        failed_attempts += 1

                zip_file.close()

            # only send the database object if the file was sent correctly
            if failed_attempts == 0:
                logger.info("Sending device backup database object to FarmMonitor")
                message = FileMessage(self.device_id, 'farm_monitor', 'backup')
                message.set_db_object(pickle.dumps(backup))
                reply = self.send([pickle.dumps(message)])

                if reply:
                    reply_message = pickle.loads(reply[1])
                    logger.debug("Update response: {0}".format(reply_message.reply))

            else:
                logger.warn("To many failed attempts. Backup failed to send to Farm Monitor")

        session.close()

        return

    def perform_update(self, update_id):

        session = Session()
        standalone_configuration = session.query(SystemSetup.standalone_configuration).scalar()

        if self.device_state == 'connected':

            # request the update db_object
            logger.info("Requesting update with id: {0} from FarmMonitor".format(update_id))
            message = [self.device_id, 'perform_update', 'db_object', update_id]
            message = FileMessage(self.device_id, 'farm_monitor', 'perform_update')
            message.set_db_object(update_id)
            reply = self.send([pickle.dumps(message)])

            if reply:
                reply_message = pickle.loads(reply[1])
                logger.debug("Update response from farm_monitor")

                if reply_message.data:
                    update = pickle.loads(reply_message.data)

                else:
                    logger.warn("incorrect response in perform_update: {0}".format(reply[0]))
                    session.close()
                    return

                failed_attempts = 0

                # don't request the file if we are a combined configuration
                if standalone_configuration:

                    if not os.path.exists(update.filepath):
                        os.makedirs(update.filepath)

                    bytes_received = 0
                    more_to_send = True
                    new_file = open(update.filepath + update.filename, 'a')

                    while more_to_send and failed_attempts < 3:
                        logger.info("Requesting update file from FarmMonitor")
                        message = FileMessage(self.device_id, 'farm_monitor', 'perform_update')
                        message.request_file(update_id, str(bytes_received))
                        reply = self.send([pickle.dumps(message)])

                        if reply:
                            reply_message = pickle.loads(reply[1])
                            logger.debug("Update response from FarmMonitor")
                            failed_attempts = 0
                            # in this case, reply_message.filepath is really a boolean.
                            # reusing the FileMessage object
                            more_to_send = reply_message.filepath
                            data = reply_message.data

                            new_file.seek(bytes_received)
                            new_file.write(data)
                            bytes_received += len(data)

                        else:
                            logger.warn("Failed attempt in requesting update file")
                            failed_attempts += 1

                    new_file.close()

                # compare the hash of the file
                sha256 = hashlib.sha256()
                with open(update.filepath + update.filename, 'rb') as f:
                    for block in iter(lambda: f.read(2048), b''):
                        sha256.update(block)

                if update.sha256 == sha256.hexdigest():
                    logger.info("Update file successfully received")
                    print('update received good')
                    session.merge(update)
                    session.commit()
                else:
                    logger.warn("Update file hash does not match. Update failed")
                    session.close()
                    return

                # now perform the actual update
                run_update(update.id, 'farm_device')

            else:
                logger.warn("No reply when requesting update db_object from FarMonitor.")

        session.close()
        return
예제 #6
0
class MDPWorker(ZMQStream):
    def __init__(self, broker, service, io_loop=None):
        """Create and setup an MDP worker.
           @param broker A string containing the broker's URL
           @param service A string containing the service name
           @param io_loop An existing I/O loop object. If None, the default will be used.
        """
        self.service=service
        self._broker = broker

        self.ctx = zmq.Context()
        sock = self.ctx.socket(zmq.DEALER)
        ZMQStream.__init__(self, sock, io_loop)
        # last watchdog timer tick
        self.watchdog = 0
        # connection callback one-shot
        self._conncb = DelayedCallback(self.send_ready, 3000, self.io_loop)
        # heartbeat callback..runs continuous when connected
        self._hbcb = PeriodicCallback(self.send_heartbeat, 2000, self.io_loop)
        # number of connection attempts
        self._conn_attempt = 0
        # waiting to connect state
        self._waiting_to_connect = True
        # have we been disconnected? (flags a reconnect attempt)
        self.disconnected = False

        # connect the socket and send a READY when the io_loop starts
        self.connect(self._broker)
        self._conncb.start()

    def reset_watchdog(self):
        """Private method used to reset the HEARTBEAT watchdog
        """
        self.watchdog = time.time()

    def disconnect(self):
        """Disconnect from the broker.
        """
        logging.info("Disconnected from broker")
        self.on_recv(None) # stop message processing
        self._conncb.stop() # stop any pending reconnect
        self._hbcb.stop() # stop heartbeats
        self.disconnected = True
        self.io_loop.stop() # stop the I/O loop. If it's used by something else, the caller can restart it

    def reconnect(self):
        """Try to reconnect to the broker.
        """
        if self.disconnected: # don't try and reconnect, we got an explicit disconnect
            return
        logging.info("Attempting to reconnect to broker")
        self._hbcb.stop()
        self._conn_attempt = 0
        self._waiting_to_connect = True
        try:
            self.connect(self._broker)
        except ZMQError:
            logging.exception()
            self.io_loop.stop()
            return
        self._conncb.start()

    def send_ready(self):
        """Send a READY message.
        """
        if not self._waiting_to_connect:
            # connected already
            return
        if self.disconnected: # don't try and connect, we got an explicit disconnect
            return
        logging.debug("Sending READY")
        if self._conn_attempt >= 10:
            logging.error("10 connection attempts have failed. Giving up.")
            return
        self._conn_attempt += 1
        logging.debug("Connection attempt %i" % self._conn_attempt)
        rdy = [b'', MDPW_VER, b'\x01', self.service]
        self.on_recv(self.on_message)
        self.send_multipart(rdy)
        # There is no reply to READY so
        # we must assume we are connected unless we see a DISCONNECT
        self._waiting_to_connect = False
        self._disconed = False
        self.reset_watchdog()
        self._hbcb.start()

    def send_reply(self, client, msg, partial=False):
        """Send a reply to a client. This is typically called from on_request()
           @param client The client identifier as passed to on_request()
           @param msg The message to send to the client. If this is a list, it's appended to the multipart;
                      otherwise it is converted to a string and sent as a single frame.
           @param partial If this is True, the message is sent as a PARTIAL and at least one
                          more call must be made to send_reply(). Otherwise a FINAL is sent and
                          not more calls should be made to send_reply() until another request is processed.
                   
        """
        self._hbcb.stop() # stop while sending other messages
        if partial:
            rply = [b'', MDPW_VER, b'\x03', client, b'']
        else:
            rply = [b'', MDPW_VER, b'\x04', client, b'']
        if isinstance(msg, list):
            rply.extend(msg)
        else:
            rply.append(msg)
        try:
            logging.debug("Sending reply: %s" % msg_str(rply))
            self.send_multipart(rply)
        except BaseException,e:
            logging.error("Error sending reply: " % e)
        self._hbcb.start() # restart heartbeats
예제 #7
0
class MNClient(MN_object):
    """Class for the MN client side.

    Thin asynchronous encapsulation of a zmq.REQ socket.
    Provides a :func:`request` method with optional timeout.

    :param context:  the ZeroMQ context to create the socket in.
    :type context:   zmq.Context
    :param endpoint: the endpoint to connect to.
    :type endpoint:  str
    :param service:  the service the client should use
    :type service:   str
    """

    _proto_version = CLIENT_PROTO

    def __init__(self, context, endpoint, service):
        """Initialize the MNClient.
        """
        self.context = context
        self.service = service
        self.endpoint = endpoint
        self.can_send = True
        self._proto_prefix = ['', CLIENT_PROTO, service]
        self._tmo = None
        self.timed_out = False
        self._create_stream()
        return

    def _create_stream(self):
        """Helper for opening a stream.
        """
        socket = self.context.socket(zmq.DEALER)
        ioloop = IOLoop.instance()
        self.stream = ZMQStream(socket, ioloop)
        self.stream.on_recv(self._on_message)
        self.stream.socket.setsockopt(zmq.LINGER, 0)
        self.stream.connect(self.endpoint)

    def shutdown(self):
        """Method to deactivate the client connection completely.

        Will delete the stream and the underlying socket.

        .. warning:: The instance MUST not be used after :func:`shutdown` has been called.

        :rtype: None
        """
        if not self.stream:
            return
        self.stream.socket.close()
        self.stream.close()
        self.stream = None
        return

    def request(self, msg, timeout=None):
        """Send the given message.

        :param msg:     message parts to send.
        :type msg:      list of str
        :param timeout: time to wait in milliseconds.
        :type timeout:  int

        :rtype None:
        """
        if not self.can_send:
            raise InvalidStateError()
        if isinstance(msg, bytes):
            msg = [msg]
        # prepare full message
        # to_send = self._proto_prefix[:]
        # to_send.extend(msg)
        to_send = msg
        if self.stream.closed():
            self._create_stream()
            # TODO check this
        self.stream.send_multipart(to_send)
        self.can_send = False
        if timeout:
            self._start_timeout(timeout)
        return

    def _on_timeout(self):
        """Helper called after timeout.
        """
        self.timed_out = True
        self._tmo = None
        self.on_timeout()
        return

    def _start_timeout(self, timeout):
        """Helper for starting the timeout.

        :param timeout:  the time to wait in milliseconds.
        :type timeout:   int
        """
        self._tmo = DelayedCallback(self._on_timeout, timeout)
        self._tmo.start()
        return

    def _on_message(self, msg):
        """Helper method called on message receive.

        :param msg:   list of message parts.
        :type msg:    list of str
        """
        if self._tmo:
            # disable timout
            self._tmo.stop()
            self._tmo = None
        # setting state before invoking on_message, so we can request from there
        self.can_send = True
        self.on_message(msg)
        return

    def on_message(self, msg):
        """Public method called when a message arrived.

        .. note:: Does nothing. Should be overloaded!
        """
        pass

    def on_timeout(self):
        """Public method called when a timeout occurred.

        .. note:: Does nothing. Should be overloaded!
        """
        pass
예제 #8
0
class GrainBinService(object):

    SERVICE_NAME = 'grain_bin'

    def __init__(self, context, device_id, address):

        self.context = context
        self.address = address
        self.device_id = device_id

        self.socket = None

        # delay = seconds_till_next('hour', duration=1) + 10  # 10 second buffer
        delay = 120  # 2 minutes after startup
        self.updater = DelayedCallback(self.update, delay * 1000)
        self.updater.start()
        return

    def shutdown(self):
        self.updater.stop()
        return

    def update(self):
        self.updater.stop()

        if internal_dealer.check_connection():
            logger.info("Sending update for all grainbins")
            socket = self.context.socket(zmq.REQ)
            socket.setsockopt(zmq.LINGER, 0)
            socket.connect(self.address)

            update = GrainbinUpdate(self.device_id, 'farm_monitor')
            update.create()
            message = [pickle.dumps(update)]

            reply = mdp_request(socket, 'grainbin', message, 5)
            if reply:
                message = pickle.loads(reply[1])
                logger.debug("reply from farm monitor for update: {0}".format(message.reply))

        # delay = seconds_till_next('hour', duration=1) + 10  # 10 second buffer
        delay = 60 * 60  # send next update in one hour
        self.updater = DelayedCallback(self.update, delay * 1000)
        self.updater.start()
        return

    def update_individual(self, bus_number):

        if internal_dealer.check_connection():
            logger.info("Sending update for grainbin: {0}".format(bus_number))
            socket = self.context.socket(zmq.REQ)
            socket.setsockopt(zmq.LINGER, 0)
            socket.connect(self.address)

            update = GrainbinUpdate(self.device_id, 'farm_monitor')
            update.create(bus_number=bus_number)
            message = [pickle.dumps(update)]

            reply = mdp_request(socket, 'grainbin', message, 5)
            if reply:
                message = pickle.loads(reply[1])
                logger.debug("reply from farm monitor for update: {0}".format(message.reply))

        return

    def rescan(self, bus_number):

        logger.info("sending rescan for grainbin: {0}".format(bus_number))
        info = GrainbinInfo(self.device_id, 'farm_monitor')
        info.create()

        message = [pickle.dumps(info)]

        socket = self.context.socket(zmq.REQ)
        socket.setsockopt(zmq.LINGER, 0)
        socket.connect(self.address)

        reply = mdp_request(socket, 'grainbin', message, 5)
        if reply:
            message = pickle.loads(reply[1])
            logger.debug("reply from farm monitor for rescan: {0}".format(message.reply))

        return