holgern/beem

View on GitHub
beemapi/node.py

Summary

Maintainability
C
1 day
Test Coverage
B
82%
# -*- coding: utf-8 -*-
import json
import re
import time
import logging
from .exceptions import (
    UnauthorizedError, RPCConnection, RPCError, NumRetriesReached, CallRetriesReached
)
log = logging.getLogger(__name__)


class Node(object):
    def __init__(
        self,
        url
    ):
        self.url = url
        self.error_cnt = 0
        self.error_cnt_call = 0

    def __repr__(self):
        return self.url


class Nodes(list):
    """Stores Node URLs and error counts"""
    def __init__(self, urls, num_retries, num_retries_call):
        self.set_node_urls(urls)
        self.num_retries = num_retries
        self.num_retries_call = num_retries_call

    def set_node_urls(self, urls):
        if isinstance(urls, str):
            url_list = re.split(r",|;", urls)
            if url_list is None:
                url_list = [urls]
        elif isinstance(urls, Nodes):
            url_list = [urls[i].url for i in range(len(urls))]
        elif isinstance(urls, (list, tuple, set)):
            url_list = urls
        elif urls is not None:
            url_list = [urls]
        else:
            url_list = []        
        super(Nodes, self).__init__([Node(x) for x in url_list])
        self.current_node_index = -1
        self.freeze_current_node = False        

    def __iter__(self):
        return self

    def __next__(self):
        next_node_count = 0
        if self.freeze_current_node:
            return self.url
        while next_node_count == 0 and (self.num_retries < 0 or self.node.error_cnt < self.num_retries):
            self.current_node_index += 1
            if self.current_node_index >= self.working_nodes_count:
                self.current_node_index = 0
            next_node_count += 1
            if next_node_count > self.working_nodes_count + 1:
                raise StopIteration
        return self.url

    next = __next__  # Python 2

    def export_working_nodes(self):
        nodes_list = []
        for i in range(len(self)):
            if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
                nodes_list.append(self[i].url)
        return nodes_list

    def __repr__(self):
        nodes_list = self.export_working_nodes()
        return str(nodes_list)

    @property
    def working_nodes_count(self):
        n = 0
        if self.freeze_current_node:
            i = self.current_node_index
            if self.current_node_index < 0:
                i = 0
            if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
                n += 1
            return n
        for i in range(len(self)):
            if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
                n += 1
        return n

    @property
    def url(self):
        if self.node is None:
            return ''
        return self.node.url

    @property
    def node(self):
        if self.current_node_index < 0:
            return self[0]
        return self[self.current_node_index]

    @property
    def error_cnt(self):
        if self.node is None:
            return 0
        return self.node.error_cnt

    @property
    def error_cnt_call(self):
        if self.node is None:
            return 0
        return self.node.error_cnt_call

    @property
    def num_retries_call_reached(self):
        return self.error_cnt_call >= self.num_retries_call

    def disable_node(self):
        """Disable current node"""
        if self.node is not None and self.num_retries_call >= 0:
            self.node.error_cnt_call = self.num_retries_call

    def increase_error_cnt(self):
        """Increase node error count for current node"""
        if self.node is not None:
            self.node.error_cnt += 1

    def increase_error_cnt_call(self):
        """Increase call error count for current node"""
        if self.node is not None:
            self.node.error_cnt_call += 1

    def reset_error_cnt_call(self):
        """Set call error count for current node to zero"""
        if self.node is not None:
            self.node.error_cnt_call = 0

    def reset_error_cnt(self):
        """Set node error count for current node to zero"""
        if self.node is not None:
            self.node.error_cnt = 0

    def sleep_and_check_retries(self, errorMsg=None, sleep=True, call_retry=False, showMsg=True):
        """Sleep and check if num_retries is reached"""
        if errorMsg:
            log.warning("Error: {}".format(errorMsg))
        if call_retry:
            cnt = self.error_cnt_call
            if (self.num_retries_call >= 0 and self.error_cnt_call > self.num_retries_call):
                raise CallRetriesReached()
        else:
            cnt = self.error_cnt
            if (self.num_retries >= 0 and self.error_cnt > self.num_retries):
                raise NumRetriesReached()

        if showMsg:
            if call_retry:
                log.warning("Retry RPC Call on node: %s (%d/%d) \n" % (self.url, cnt, self.num_retries_call))
            else:
                log.warning("Lost connection or internal error on node: %s (%d/%d) \n" % (self.url, cnt, self.num_retries))
        if not sleep:
            return
        if cnt < 1:
            sleeptime = 0
        elif cnt < 10:
            sleeptime = (cnt - 1) * 1.5 + 0.5
        else:
            sleeptime = 10
        if sleeptime:
            log.warning("Retrying in %d seconds\n" % sleeptime)
            time.sleep(sleeptime)