예제 #1
0
파일: main.py 프로젝트: zeta1999/karton
def get_service_binds(config: Config) -> List[KartonBind]:
    redis = StrictRedis(decode_responses=True, **config.redis_config)
    replica_no: Dict[Any, int] = Counter()

    # count replicas for each identity
    for client in redis.client_list():
        replica_no[client["name"]] += 1

    binds = redis.hgetall("karton.binds")
    services = []
    for identity, data in binds.items():
        val = json.loads(data)
        # karton 2.x compatibility :(
        if isinstance(val, list):
            val = val[0]

        services.append(
            KartonBind(
                identity,
                replica_no[identity],
                val.get("persistent", True),
                val.get("version", "2.x.x"),
                val.get("filters", []),
            ))
    return services
예제 #2
0
파일: backend.py 프로젝트: zeta1999/karton
class KartonBackend:
    def __init__(self, config):
        self.config = config
        self.redis = StrictRedis(
            host=config["redis"]["host"],
            port=int(config["redis"].get("port", 6379)),
            decode_responses=True,
        )
        self.minio = Minio(
            config["minio"]["address"],
            access_key=config["minio"]["access_key"],
            secret_key=config["minio"]["secret_key"],
            secure=bool(int(config["minio"].get("secure", True))),
        )

    @property
    def default_bucket_name(self) -> str:
        return self.config.minio_config["bucket"]

    @staticmethod
    def get_queue_name(identity: str, priority: TaskPriority) -> str:
        """
        Return Redis routed task queue name for given identity and priority

        :param identity: Karton service identity
        :param priority: Queue priority (TaskPriority enum value)
        :return: Queue name
        """
        return f"karton.queue.{priority.value}:{identity}"

    @staticmethod
    def get_queue_names(identity: str) -> List[str]:
        """
        Return all Redis routed task queue names for given identity,
        ordered by priority (descending). Used internally by Consumer.

        :param identity: Karton service identity
        :return: List of queue names
        """
        return [
            identity,  # Backwards compatibility (2.x.x)
            KartonBackend.get_queue_name(identity, TaskPriority.HIGH),
            KartonBackend.get_queue_name(identity, TaskPriority.NORMAL),
            KartonBackend.get_queue_name(identity, TaskPriority.LOW),
        ]

    @staticmethod
    def serialize_bind(bind: KartonBind) -> str:
        """
        Serialize KartonBind object (Karton service registration)

        :param bind: KartonBind object with bind definition
        :return: Serialized bind data
        """
        return json.dumps(
            {
                "info": bind.info,
                "version": bind.version,
                "filters": bind.filters,
                "persistent": bind.persistent,
            },
            sort_keys=True,
        )

    @staticmethod
    def unserialize_bind(identity: str, bind_data: str) -> KartonBind:
        """
        Deserialize KartonBind object for given identity.
        Compatible with Karton 2.x.x and 3.x.x

        :param identity: Karton service identity
        :param bind_data: Serialized bind data
        :return: KartonBind object with bind definition
        """
        bind = json.loads(bind_data)
        if isinstance(bind, list):
            # Backwards compatibility (v2.x.x)
            return KartonBind(
                identity=identity,
                info=None,
                version="2.x.x",
                persistent=not identity.endswith(".test"),
                filters=bind,
            )
        return KartonBind(
            identity=identity,
            info=bind["info"],
            version=bind["version"],
            persistent=bind["persistent"],
            filters=bind["filters"],
        )

    def get_bind(self, identity: str) -> KartonBind:
        """
        Get bind object for given identity

        :param identity: Karton service identity
        :return: KartonBind object
        """
        return self.unserialize_bind(
            identity, self.redis.hget(KARTON_BINDS_HSET, identity))

    def get_binds(self) -> List[KartonBind]:
        """
        Get all binds registered in Redis

        :return: List of KartonBind objects for subsequent identities
        """
        return [
            self.unserialize_bind(identity, raw_bind) for identity, raw_bind in
            self.redis.hgetall(KARTON_BINDS_HSET).items()
        ]

    def register_bind(self, bind: KartonBind) -> Optional[KartonBind]:
        """
        Register bind for Karton service and return the old one

        :param bind: KartonBind object with bind definition
        :return: Old KartonBind that was registered under this identity
        """
        with self.redis.pipeline(transaction=True) as pipe:
            pipe.hget(KARTON_BINDS_HSET, bind.identity)
            pipe.hset(KARTON_BINDS_HSET, bind.identity,
                      self.serialize_bind(bind))
            old_serialized_bind, _ = pipe.execute()

        if old_serialized_bind:
            return self.unserialize_bind(bind.identity, old_serialized_bind)
        else:
            return None

    def unregister_bind(self, identity: str) -> None:
        """
        Removes bind for identity
        :param bind: Identity to be unregistered
        """
        self.redis.hdel(KARTON_BINDS_HSET, identity)

    def set_consumer_identity(self, identity: str) -> None:
        """
        Sets identity for current Redis connection
        """
        return self.redis.client_setname(identity)

    def get_online_consumers(self) -> Dict[str, List[str]]:
        """
        Gets all online consumer identities

        :return: Dictionary {identity: [list of clients]}
        """
        bound_identities = defaultdict(list)
        for client in self.redis.client_list():
            bound_identities[client["name"]].append(client)
        return bound_identities

    def get_task(self, task_uid: str) -> Optional[Task]:
        """
        Get task object with given identifier

        :param task_uid: Task identifier
        :return: Task object
        """
        task_data = self.redis.get(f"{KARTON_TASK_NAMESPACE}:{task_uid}")
        if not task_data:
            return None
        return Task.unserialize(task_data, backend=self)

    def get_tasks(self, task_uid_list: List[str]) -> List[Task]:
        """
        Get multiple tasks for given identifier list

        :param task_uid_list: List of task identifiers
        :return: List of task objects
        """
        task_list = self.redis.mget([
            f"{KARTON_TASK_NAMESPACE}:{task_uid}" for task_uid in task_uid_list
        ])
        return [
            Task.unserialize(task_data, backend=self)
            for task_data in task_list if task_data is not None
        ]

    def get_all_tasks(self) -> List[Task]:
        """
        Get all tasks registered in Redis

        :return: List with Task objects
        """
        tasks = self.redis.keys(f"{KARTON_TASK_NAMESPACE}:*")
        return [
            Task.unserialize(task_data) for task_data in self.redis.mget(tasks)
            if task_data is not None
        ]

    def register_task(self, task: Task) -> None:
        """
        Register task in Redis.

        Consumer should register only Declared tasks.
        Status change should be done using set_task_status.

        :param task: Task object
        """
        self.redis.set(f"{KARTON_TASK_NAMESPACE}:{task.uid}", task.serialize())

    def set_task_status(self,
                        task: Task,
                        status: TaskState,
                        consumer: Optional[str] = None) -> None:
        """
        Request task status change to be applied by karton-system

        :param task: Task object
        :param status: New task status (TaskState)
        :param consumer: Consumer identity
        """
        self.redis.rpush(
            KARTON_OPERATIONS_QUEUE,
            json.dumps({
                "status": status.value,
                "identity": consumer,
                "task": task.serialize(),
                "type": "operation",
            }),
        )

    def delete_task(self, task: Task) -> None:
        """
        Remove task from Redis

        :param task: Task object
        """
        self.redis.delete(f"{KARTON_TASK_NAMESPACE}:{task.uid}")

    def get_task_queue(self, queue: str) -> List[Task]:
        """
        Return all tasks in provided queue

        :param queue: Queue name
        :return: List with Task objects contained in queue
        """
        task_uids = self.redis.lrange(queue, 0, -1)
        return self.get_tasks(task_uids)

    def get_task_ids_from_queue(self, queue: str) -> List[str]:
        """
        Return all task UIDs in a queue

        :param queue: Queue name
        :return: List with task identifiers contained in queue
        """
        return self.redis.lrange(queue, 0, -1)

    def remove_task_queue(self, queue: str) -> List[Task]:
        """
        Remove task queue with all contained tasks

        :param queue: Queue name
        :return: List with Task objects contained in queue
        """
        pipe = self.redis.pipeline()
        pipe.lrange(queue, 0, -1)
        pipe.delete(queue)
        return self.get_tasks(pipe.execute()[0])

    def produce_unrouted_task(self, task: Task) -> None:
        """
        Add given task to unrouted task (``karton.tasks``) queue

        Task must be registered before with :py:meth:`register_task`

        :param task: Task object
        """
        self.redis.rpush(KARTON_TASKS_QUEUE, task.uid)

    def produce_routed_task(self, identity: str, task: Task) -> None:
        """
        Add given task to routed task queue of given identity

        Task must be registered using :py:meth:`register_task`

        :param identity: Karton service identity
        :param task: Task object
        """
        self.redis.rpush(self.get_queue_name(identity, task.priority),
                         task.uid)

    def consume_queues(self,
                       queues: Union[str, List[str]],
                       timeout: int = 0) -> Optional[Tuple[str, str]]:
        """
        Get item from queues (ordered from the most to the least prioritized)
        If there are no items, wait until one appear.

        :param queues: Redis queue name or list of names
        :param timeout: Waiting for item timeout (default: 0 = wait forever)
        :return: Tuple of [queue_name, item] objects or None if timeout has been reached
        """
        return self.redis.blpop(queues, timeout=timeout)

    def consume_routed_task(self,
                            identity: str,
                            timeout: int = 5) -> Optional[Task]:
        """
        Get routed task for given consumer identity.

        If there are no tasks, blocks until new one appears or timeout is reached.

        :param identity: Karton service identity
        :param timeout: Waiting for task timeout (default: 5)
        :return: Task object
        """
        item = self.consume_queues(
            self.get_queue_names(identity),
            timeout=timeout,
        )
        if not item:
            return None
        queue, data = item
        return self.get_task(data)

    @staticmethod
    def _log_channel(logger_name: Optional[str], level: Optional[str]) -> str:
        return ".".join(
            [KARTON_LOG_CHANNEL, (level or "*").lower(), logger_name or "*"])

    def produce_log(
        self,
        log_record: Dict[str, Any],
        logger_name: str,
        level: str,
    ) -> bool:
        """
        Push new log record to the logs channel

        :param log_record: Dict with log record
        :param logger_name: Logger name
        :param level: Log level
        :return: True if any active log consumer received log record
        """
        return (self.redis.publish(self._log_channel(logger_name, level),
                                   json.dumps(log_record)) > 0)

    def consume_log(
        self,
        timeout: int = 5,
        logger_filter: Optional[str] = None,
        level: Optional[str] = None,
    ) -> Iterator[Optional[Dict[str, Any]]]:
        """
        Subscribe to logs channel and yield subsequent log records
        or None if timeout has been reached.

        If you want to subscribe only to a specific logger name
        and/or log level, pass them via logger_filter and level arguments.

        :param timeout: Waiting for log record timeout (default: 5)
        :param logger_filter: Filter for name of consumed logger
        :param level: Log level
        :return: Dict with log record
        """
        with self.redis.pubsub() as pubsub:
            pubsub.psubscribe(self._log_channel(logger_filter, level))
            while pubsub.subscribed:
                item = pubsub.get_message(ignore_subscribe_messages=True,
                                          timeout=timeout)
                if item and item["type"] == "pmessage":
                    body = json.loads(item["data"])
                    if "task" in body and isinstance(body["task"], str):
                        body["task"] = json.loads(body["task"])
                    yield body
                yield None

    def increment_metrics(self, metric: KartonMetrics, identity: str) -> None:
        """
        Increments metrics for given operation type and identity

        :param metric: Operation metric type
        :param identity: Related Karton service identity
        """
        self.redis.hincrby(metric.value, identity, 1)

    def upload_object(
        self,
        bucket: str,
        object_uid: str,
        content: Union[bytes, BinaryIO],
        length: int = None,
    ) -> None:
        """
        Upload resource object to underlying object storage (Minio)

        :param bucket: Bucket name
        :param object_uid: Object identifier
        :param content: Object content as bytes or file-like stream
        :param length: Object content length (if file-like object provided)
        """
        if isinstance(content, bytes):
            length = len(content)
            content = BytesIO(content)
        self.minio.put_object(bucket, object_uid, content, length)

    def upload_object_from_file(self, bucket: str, object_uid: str,
                                path: str) -> None:
        """
        Upload resource object file to underlying object storage

        :param bucket: Bucket name
        :param object_uid: Object identifier
        :param path: Path to the object content
        """
        self.minio.fput_object(bucket, object_uid, path)

    def get_object(self, bucket: str, object_uid: str) -> HTTPResponse:
        """
        Get resource object stream with the content.

        Returned response should be closed after use to release network resources.
        To reuse the connection, it's required to call `response.release_conn()`
        explicitly.

        :param bucket: Bucket name
        :param object_uid: Object identifier
        :return: Response object with content
        """
        return self.minio.get_object(bucket, object_uid)

    def download_object(self, bucket: str, object_uid: str) -> bytes:
        """
        Download resource object from object storage.

        :param bucket: Bucket name
        :param object_uid: Object identifier
        :return: Content bytes
        """
        reader = self.minio.get_object(bucket, object_uid)
        try:
            return reader.read()
        finally:
            reader.release_conn()
            reader.close()

    def download_object_to_file(self, bucket: str, object_uid: str,
                                path: str) -> None:
        """
        Download resource object from object storage to file

        :param bucket: Bucket name
        :param object_uid: Object identifier
        :param path: Target file path
        """
        self.minio.fget_object(bucket, object_uid, path)

    def list_objects(self, bucket: str) -> List[str]:
        """
        List identifiers of stored resource objects

        :param bucket: Bucket name
        :return: List of object identifiers
        """
        return [
            object.object_name for object in self.minio.list_objects(bucket)
        ]

    def remove_object(self, bucket: str, object_uid: str) -> None:
        """
        Remove resource object from object storage

        :param bucket: Bucket name
        :param object_uid: Object identifier
        """
        self.minio.remove_object(bucket, object_uid)

    def check_bucket_exists(self, bucket: str, create: bool = False) -> bool:
        """
        Check if bucket exists and optionally create it if it doesn't.

        :param bucket: Bucket name
        :param create: Create bucket if doesn't exist
        :return: True if bucket exists yet
        """
        if self.minio.bucket_exists(bucket):
            return True
        if create:
            self.minio.make_bucket(bucket)
        return False
예제 #3
0
class Storage:
    def __init__(self, bot, config_prefix):
        """
        Create new storage for bot

        :param bot: Leonard object
        :param config_prefix: prefix of variables in config. Default - 'LEONARD_'
        :return:
        """

        # Connect to Redis.
        # If we had problems with Redis - just set self.redis to None.
        # Not redis-required modules must work without Redis.

        # We get parameters for redis connection from bot's config.
        self.redis = StrictRedis(
            host=bot.config.get('{}REDIS_HOST'.format(config_prefix), 'localhost'),
            port=bot.config.get('{}REDIS_PORT'.format(config_prefix), '6379'),
            db=bot.config.get('{}REDIS_DB'.format(config_prefix), '0')
        )
        try:
            # Check Redis connection
            self.redis.client_list()
        except Exception as error:
            logger.error_message('Error while connecting Redis:')
            logger.error_message(str(error))
            self.redis = None

    def get(self, key, default_value=None):
        """
        Get value from redis storage

        :param key: string, redis key for needed value
        :param default_value: string, value that returns if
                              key not found or redis isn't
                              connected to this bot
        :return: value with that key or default value
        """
        # If we had problems with redis connection,
        # return None or other default value
        if not self.redis:
            logger.warning_message("Redis not available for {} key".format(
                key
            ))
            return default_value

        value = self.redis.get(key)
        if value is not None:
            return value
        else:
            return default_value

    def set(self, key, value):
        """
        Set key to value in redis storage

        :param key: string
        :param value: string
        :return:
        """
        if not self.redis:
            logger.warning_message("{} key didn't save in storage".format(
                key
            ))
            return None

        return self.redis.set(key, value)

    def get_json(self, key, default_value=None):
        """
        Get value from Redis and parse JSON in it

        :param key: str
        :param default_value: string, value that returns if
                              key not found or redis isn't
                              connected to this bot
        :return: list/dict
        """
        value = self.get(key, default_value)
        if not value:
            return value
        if type(value) not in [list, dict]:
            return json.loads(value.decode('utf-8'))
        return value

    def set_json(self, key, value):
        """
        Dump value into json and save it in Redis

        :param key: string
        :param value: list/dict
        :return:
        """
        json_value = json.dumps(value)
        return self.set(key, json_value)
예제 #4
0
class Storage:
    def __init__(self, bot):
        """
        Create new storage for bot

        :param bot: Bot object
        :return:
        """
        self.bot = bot

        # Connect to Redis.
        # If we had problems with Redis - just set self.redis to None.
        # Not redis-required modules must work without Redis.

        # We get parameters for redis connection from bot's config.
        self.redis = StrictRedis(
            host=bot.config.get('SHELDON_REDIS_HOST', 'localhost'),
            port=bot.config.get('SHELDON_REDIS_PORT', '6379'),
            db=bot.config.get('SHELDON_REDIS_DB', '0')
        )
        try:
            # Check Redis connection
            self.redis.client_list()
        except Exception as error:
            logger.error_message('Error while connecting Redis:')
            logger.error_message(str(error))
            self.redis = None

    def get(self, key, default_value=None):
        """
        Get value from redis storage

        :param key: string, redis key for needed value
        :param default_value: string, value that returns if
                              key not found or redis isn't
                              connected to this bot
        :return: value with that key or default value
        """
        # If we had problems with redis connection,
        # return None or other default value
        if not self.redis:
            logger.warning_message("Redis not available for {} key".format(
                key
            ))
            return default_value

        value = self.redis.get(key)
        if value is not None:
            return value
        else:
            return default_value

    def set(self, key, value):
        """
        Set key to value in redis storage

        :param key: string
        :param value: string
        :return:
        """
        if not self.redis:
            logger.warning_message("{} key didn't save in storage".format(
                key
            ))
            return None

        return self.redis.set(key, value)