feat/#1 - applied 'black' formatting
This commit is contained in:
@@ -12,7 +12,7 @@ NULL_ROUTE = {
|
|||||||
"queue": None,
|
"queue": None,
|
||||||
"routingKey": "#",
|
"routingKey": "#",
|
||||||
"timeout": 1,
|
"timeout": 1,
|
||||||
"validUntil": 0
|
"validUntil": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,18 +49,21 @@ class AMQRouteFactory:
|
|||||||
queue=_queue,
|
queue=_queue,
|
||||||
key=_routing_key,
|
key=_routing_key,
|
||||||
timeout=_timeout,
|
timeout=_timeout,
|
||||||
valid_until=_valid_until
|
valid_until=_valid_until,
|
||||||
)
|
)
|
||||||
logging_error(
|
logging_error(
|
||||||
"AMQRoute incorrect format. AMQRoute expects input as: "
|
"AMQRoute incorrect format. AMQRoute expects input as: "
|
||||||
"'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', "
|
"'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', "
|
||||||
"got: ['%s']", data
|
"got: ['%s']",
|
||||||
|
data,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logging_error(
|
logging_error(
|
||||||
"%s: AMQRoute incorrect format. AMQRoute expects input as: "
|
"%s: AMQRoute incorrect format. AMQRoute expects input as: "
|
||||||
"'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', "
|
"'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', "
|
||||||
"got: ['%s']", err, data
|
"got: ['%s']",
|
||||||
|
err,
|
||||||
|
data,
|
||||||
)
|
)
|
||||||
return NULL_ROUTE
|
return NULL_ROUTE
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
The layer that interfaces with the CleverThis service that this Adapter is integrating with.
|
The layer that interfaces with the CleverThis service that this Adapter is integrating with.
|
||||||
Methods here will invoke the actual CleverThis Service code, which are passed as Callable object.
|
Methods here will invoke the actual CleverThis Service code, which are passed as Callable object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
@@ -13,7 +14,12 @@ from typing import Dict, List, Tuple, Callable, Coroutine, Any
|
|||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
from aio_pika.abc import AbstractRobustChannel, AbstractRobustConnection, AbstractRobustExchange, AbstractExchange
|
from aio_pika.abc import (
|
||||||
|
AbstractRobustChannel,
|
||||||
|
AbstractRobustConnection,
|
||||||
|
AbstractRobustExchange,
|
||||||
|
AbstractExchange,
|
||||||
|
)
|
||||||
from aiohttp import ClientSession, ClientResponse
|
from aiohttp import ClientSession, ClientResponse
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -28,6 +34,7 @@ from amqp.adapter.file_uploader import publish_file_to_rabbitmq
|
|||||||
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE
|
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE
|
||||||
|
|
||||||
from IPython.core import debugger
|
from IPython.core import debugger
|
||||||
|
|
||||||
debug = debugger.Pdb().set_trace
|
debug = debugger.Pdb().set_trace
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +57,9 @@ class AMQMessage(DataMessage):
|
|||||||
self.base64_body = data_message.base64_body
|
self.base64_body = data_message.base64_body
|
||||||
# Add the new field
|
# Add the new field
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.extra_data = data_message.extra_data if hasattr(data_message, 'extra_data') else {}
|
self.extra_data = (
|
||||||
|
data_message.extra_data if hasattr(data_message, "extra_data") else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_get_url(url: str) -> Tuple[str, Dict[str, str]]:
|
def parse_get_url(url: str) -> Tuple[str, Dict[str, str]]:
|
||||||
@@ -68,7 +77,9 @@ def parse_get_url(url: str) -> Tuple[str, Dict[str, str]]:
|
|||||||
_parsed = urlparse(url)
|
_parsed = urlparse(url)
|
||||||
_query_params = parse_qs(_parsed.query)
|
_query_params = parse_qs(_parsed.query)
|
||||||
# Convert values from lists to single values when there's only one value
|
# Convert values from lists to single values when there's only one value
|
||||||
_simplified_params = {k: v[0] if len(v) == 1 else v for k, v in _query_params.items()}
|
_simplified_params = {
|
||||||
|
k: v[0] if len(v) == 1 else v for k, v in _query_params.items()
|
||||||
|
}
|
||||||
return _parsed.path, _simplified_params
|
return _parsed.path, _simplified_params
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +93,7 @@ def parse_request_body(message: DataMessage):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Parsed data as a dictionary.
|
dict: Parsed data as a dictionary.
|
||||||
"""
|
"""
|
||||||
_content_type = message.headers.get('Content-Type', '')
|
_content_type = message.headers.get("Content-Type", "")
|
||||||
if not _content_type:
|
if not _content_type:
|
||||||
raise ValueError("Content-Type header is required")
|
raise ValueError("Content-Type header is required")
|
||||||
content_type = _content_type[0]
|
content_type = _content_type[0]
|
||||||
@@ -97,10 +108,13 @@ def parse_request_body(message: DataMessage):
|
|||||||
# Parse URL-encoded form data
|
# Parse URL-encoded form data
|
||||||
elif content_type == "application/x-www-form-urlencoded":
|
elif content_type == "application/x-www-form-urlencoded":
|
||||||
try:
|
try:
|
||||||
_form_data: dict = {k: v[0] if len(v) == 1 else v for k, v in parse_qs(message.body()).items()}
|
_form_data: dict = {
|
||||||
|
k: v[0] if len(v) == 1 else v
|
||||||
|
for k, v in parse_qs(message.body()).items()
|
||||||
|
}
|
||||||
if len(_form_data) == 0 and len(message.body()) > 0:
|
if len(_form_data) == 0 and len(message.body()) > 0:
|
||||||
_form_data = json.loads(message.body())
|
_form_data = json.loads(message.body())
|
||||||
if _form_data['files'] and _form_data['form']:
|
if _form_data["files"] and _form_data["form"]:
|
||||||
_form_data = process_form_data(_form_data)
|
_form_data = process_form_data(_form_data)
|
||||||
return _form_data
|
return _form_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -135,14 +149,15 @@ def parse_request_body(message: DataMessage):
|
|||||||
async def _convert_file_response(
|
async def _convert_file_response(
|
||||||
obj: FileResponse,
|
obj: FileResponse,
|
||||||
connection: AbstractRobustConnection,
|
connection: AbstractRobustConnection,
|
||||||
loop: AbstractEventLoop | None
|
loop: AbstractEventLoop | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Converts a FileResponse object to a JSON string containing the filename and stream name.
|
Converts a FileResponse object to a JSON string containing the filename and stream name.
|
||||||
The file content itself is transmitted through the named stream (which is implemented as temporary dedicated queue.
|
The file content itself is transmitted through the named stream (which is implemented as temporary dedicated queue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def _wrap_amq_api_calls(
|
async def _wrap_amq_api_calls(
|
||||||
connection: AbstractRobustConnection
|
connection: AbstractRobustConnection,
|
||||||
) -> tuple[AbstractRobustChannel, AbstractExchange, str]:
|
) -> tuple[AbstractRobustChannel, AbstractExchange, str]:
|
||||||
# Wrap calls to RabbitMQ API to make it Awaitable, so it can be run thread-safe with correct event loop
|
# Wrap calls to RabbitMQ API to make it Awaitable, so it can be run thread-safe with correct event loop
|
||||||
# This is needed because the RabbitMQ API is not thread-safe and needs to be called in the context of
|
# This is needed because the RabbitMQ API is not thread-safe and needs to be called in the context of
|
||||||
@@ -150,39 +165,53 @@ async def _convert_file_response(
|
|||||||
_channel = await connection.channel()
|
_channel = await connection.channel()
|
||||||
_exchange_name: str = AMQ_REPLY_TO_EXCHANGE
|
_exchange_name: str = AMQ_REPLY_TO_EXCHANGE
|
||||||
_exchange = await _channel.get_exchange(name=_exchange_name, ensure=True)
|
_exchange = await _channel.get_exchange(name=_exchange_name, ensure=True)
|
||||||
_queue = await _channel.declare_queue(exclusive=False) # Declare a unique, non-exclusive queue
|
_queue = await _channel.declare_queue(
|
||||||
|
exclusive=False
|
||||||
|
) # Declare a unique, non-exclusive queue
|
||||||
_queue_name = _queue.name
|
_queue_name = _queue.name
|
||||||
await _queue.bind(
|
await _queue.bind(
|
||||||
exchange=_exchange_name,
|
exchange=_exchange_name,
|
||||||
routing_key=_queue_name, # Use queue name as routing key
|
routing_key=_queue_name, # Use queue name as routing key
|
||||||
)
|
)
|
||||||
logging.debug(f" [*] Publishing to exchange: {_exchange_name}, q: {_queue_name}")
|
logging.debug(
|
||||||
|
f" [*] Publishing to exchange: {_exchange_name}, q: {_queue_name}"
|
||||||
|
)
|
||||||
return _channel, _exchange, _queue_name
|
return _channel, _exchange, _queue_name
|
||||||
|
|
||||||
# All calls to RabbitMQ API have to be called threadsafe, because those appear to be attached to parent
|
# All calls to RabbitMQ API have to be called threadsafe, because those appear to be attached to parent
|
||||||
# event loop, and need to execute at that event loop, not the current one
|
# event loop, and need to execute at that event loop, not the current one
|
||||||
_future = asyncio.run_coroutine_threadsafe(_wrap_amq_api_calls(connection), loop)
|
_future = asyncio.run_coroutine_threadsafe(_wrap_amq_api_calls(connection), loop)
|
||||||
while not _future.done():
|
while not _future.done():
|
||||||
await asyncio.sleep(0.1) # allow coroutine to execute. calling _future.result() will block the entire thread and the coroutine will never execute
|
await asyncio.sleep(
|
||||||
|
0.1
|
||||||
|
) # allow coroutine to execute. calling _future.result() will block the entire thread and the coroutine will never execute
|
||||||
_channel, _exchange, _routing_key = _future.result()
|
_channel, _exchange, _routing_key = _future.result()
|
||||||
# publish_file also needs RabbitMQ API, ensure the loop is passed as argument
|
# publish_file also needs RabbitMQ API, ensure the loop is passed as argument
|
||||||
(_stream_name, _file_size) = await publish_file_to_rabbitmq(_exchange, obj.path, _routing_key, loop)
|
(_stream_name, _file_size) = await publish_file_to_rabbitmq(
|
||||||
|
_exchange, obj.path, _routing_key, loop
|
||||||
|
)
|
||||||
# as above, but luckily this time we don't need to wait for the result
|
# as above, but luckily this time we don't need to wait for the result
|
||||||
_future = asyncio.run_coroutine_threadsafe(_channel.close(), loop)
|
_future = asyncio.run_coroutine_threadsafe(_channel.close(), loop)
|
||||||
return json.dumps({'files': [
|
return json.dumps(
|
||||||
{'filename': obj.filename,
|
{
|
||||||
'mediaType': 'application/octet-stream',
|
"files": [
|
||||||
'streamName': _stream_name,
|
{
|
||||||
'fileSize': _file_size
|
"filename": obj.filename,
|
||||||
}]
|
"mediaType": "application/octet-stream",
|
||||||
})
|
"streamName": _stream_name,
|
||||||
|
"fileSize": _file_size,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_likely_json(text: str) -> bool:
|
def is_likely_json(text: str) -> bool:
|
||||||
""" Quick test to see if should convert the response string to JSON format. """
|
"""Quick test to see if should convert the response string to JSON format."""
|
||||||
_text = text.strip()
|
_text = text.strip()
|
||||||
return (_text.startswith('{') and _text.endswith('}')) or \
|
return (_text.startswith("{") and _text.endswith("}")) or (
|
||||||
(_text.startswith('[') and _text.endswith(']'))
|
_text.startswith("[") and _text.endswith("]")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def call_cleverthis_api(
|
async def call_cleverthis_api(
|
||||||
@@ -190,7 +219,7 @@ async def call_cleverthis_api(
|
|||||||
args: Any,
|
args: Any,
|
||||||
api_method: Callable[[Any, Any], Coroutine],
|
api_method: Callable[[Any, Any], Coroutine],
|
||||||
success_code: int,
|
success_code: int,
|
||||||
loop: AbstractEventLoop | None = None
|
loop: AbstractEventLoop | None = None,
|
||||||
) -> DataResponse:
|
) -> DataResponse:
|
||||||
"""
|
"""
|
||||||
Handles POST and PUT requests, which typically contain possibly complex payload data, passed as a dictionary.
|
Handles POST and PUT requests, which typically contain possibly complex payload data, passed as a dictionary.
|
||||||
@@ -220,41 +249,69 @@ async def call_cleverthis_api(
|
|||||||
else:
|
else:
|
||||||
_verified_args[arg.name] = args[arg.name]
|
_verified_args[arg.name] = args[arg.name]
|
||||||
else:
|
else:
|
||||||
logging.info(f"{api_method.__name__}: Missing required argument: {arg.name}")
|
logging.info(
|
||||||
|
f"{api_method.__name__}: Missing required argument: {arg.name}"
|
||||||
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
f" [*] INVOKE ENDPOINT: {api_method.__name__}, ARGS: {', '.join(f'{k}={v}' for k, v in _verified_args.items())}")
|
f" [*] INVOKE ENDPOINT: {api_method.__name__}, ARGS: {', '.join(f'{k}={v}' for k, v in _verified_args.items())}"
|
||||||
|
)
|
||||||
_result: Any = await api_method(**_verified_args)
|
_result: Any = await api_method(**_verified_args)
|
||||||
logging.info(f" [*] ENDPOINT RESULT: {api_method.__name__}, RESULT: {str(_result)}")
|
logging.info(
|
||||||
|
f" [*] ENDPOINT RESULT: {api_method.__name__}, RESULT: {str(_result)}"
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(_result, FileResponse):
|
if isinstance(_result, FileResponse):
|
||||||
_json_result, _content_type = await _convert_file_response(
|
_json_result, _content_type = (
|
||||||
_result, message.connection, loop
|
await _convert_file_response(_result, message.connection, loop),
|
||||||
), 'application/octet-stream'
|
"application/octet-stream",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_json_result = _result
|
_json_result = _result
|
||||||
_content_type = 'application/json'
|
_content_type = "application/json"
|
||||||
|
|
||||||
_binary_result = _json_result.encode('utf-8') if hasattr(
|
_binary_result = (
|
||||||
_json_result, 'encode'
|
_json_result.encode("utf-8")
|
||||||
) else _json_result.body if hasattr(
|
if hasattr(_json_result, "encode")
|
||||||
_json_result, 'body'
|
else (
|
||||||
) else _json_result.read() if hasattr(_json_result, 'read') else _json_result
|
_json_result.body
|
||||||
logging.info(f" [x] ALL DONE in call_cleverthis_api id:{message.id}, resp code:{success_code}")
|
if hasattr(_json_result, "body")
|
||||||
return DataResponseFactory.of(message.id, success_code, _content_type, _binary_result, message.trace_info)
|
else (
|
||||||
|
_json_result.read()
|
||||||
|
if hasattr(_json_result, "read")
|
||||||
|
else _json_result
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
f" [x] ALL DONE in call_cleverthis_api id:{message.id}, resp code:{success_code}"
|
||||||
|
)
|
||||||
|
return DataResponseFactory.of(
|
||||||
|
message.id, success_code, _content_type, _binary_result, message.trace_info
|
||||||
|
)
|
||||||
except HTTPException as http_error:
|
except HTTPException as http_error:
|
||||||
_content = str(http_error.detail)
|
_content = str(http_error.detail)
|
||||||
if not is_likely_json(_content):
|
if not is_likely_json(_content):
|
||||||
_content = '{"error": "' + _content + '"}'
|
_content = '{"error": "' + _content + '"}'
|
||||||
logging_error(f"Service REST endpoint invocation failed: {http_error}")
|
logging_error(f"Service REST endpoint invocation failed: {http_error}")
|
||||||
return DataResponseFactory.of(message.id, http_error.status_code, "application/json",
|
return DataResponseFactory.of(
|
||||||
_content.encode('utf-8'), message.trace_info)
|
message.id,
|
||||||
|
http_error.status_code,
|
||||||
|
"application/json",
|
||||||
|
_content.encode("utf-8"),
|
||||||
|
message.trace_info,
|
||||||
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logging_error(f"Service endpoint invocation failed: {error}")
|
logging_error(f"Service endpoint invocation failed: {error}")
|
||||||
_content = str(error)
|
_content = str(error)
|
||||||
if not is_likely_json(_content):
|
if not is_likely_json(_content):
|
||||||
_content = '{"error": "' + _content + '"}'
|
_content = '{"error": "' + _content + '"}'
|
||||||
return DataResponseFactory.of(message.id, 500, "application/json",
|
return DataResponseFactory.of(
|
||||||
_content.encode('utf-8'), message.trace_info)
|
message.id,
|
||||||
|
500,
|
||||||
|
"application/json",
|
||||||
|
_content.encode("utf-8"),
|
||||||
|
message.trace_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============= CleverThisServiceAdapter =============
|
# ============= CleverThisServiceAdapter =============
|
||||||
@@ -266,7 +323,9 @@ class CleverThisServiceAdapter:
|
|||||||
and override the methods to handle the specific API calls.
|
and override the methods to handle the specific API calls.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, amq_configuration: AMQConfiguration, session: ClientSession = None):
|
def __init__(
|
||||||
|
self, amq_configuration: AMQConfiguration, session: ClientSession = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initializes the CleverThisServiceAdapter with the given AMQ configuration and optional HTTP session.
|
Initializes the CleverThisServiceAdapter with the given AMQ configuration and optional HTTP session.
|
||||||
The HTTP session is for the case when the adapter uses loose coupling with the CleverThis service via
|
The HTTP session is for the case when the adapter uses loose coupling with the CleverThis service via
|
||||||
@@ -277,8 +336,13 @@ class CleverThisServiceAdapter:
|
|||||||
self.service_host = self.amq_configuration.dispatch.service_host
|
self.service_host = self.amq_configuration.dispatch.service_host
|
||||||
self.service_port = self.amq_configuration.dispatch.service_port
|
self.service_port = self.amq_configuration.dispatch.service_port
|
||||||
self.loop: AbstractEventLoop | None = None
|
self.loop: AbstractEventLoop | None = None
|
||||||
self.require_authenticated_user = eval(
|
self.require_authenticated_user = (
|
||||||
self.amq_configuration.amq_adapter.require_authenticated_user.capitalize()) if self.amq_configuration.amq_adapter.require_authenticated_user.strip() else True
|
eval(
|
||||||
|
self.amq_configuration.amq_adapter.require_authenticated_user.capitalize()
|
||||||
|
)
|
||||||
|
if self.amq_configuration.amq_adapter.require_authenticated_user.strip()
|
||||||
|
else True
|
||||||
|
)
|
||||||
|
|
||||||
def get_content_type(self, message_headers: Dict[str, List[str]]) -> str:
|
def get_content_type(self, message_headers: Dict[str, List[str]]) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -287,9 +351,9 @@ class CleverThisServiceAdapter:
|
|||||||
return: The content type of the message.
|
return: The content type of the message.
|
||||||
"""
|
"""
|
||||||
for header, values in message_headers.items():
|
for header, values in message_headers.items():
|
||||||
if header.lower() == 'content-type':
|
if header.lower() == "content-type":
|
||||||
return values[0].split(';')[0]
|
return values[0].split(";")[0]
|
||||||
return 'application/json'
|
return "application/json"
|
||||||
|
|
||||||
async def get_current_user(self, token: str) -> object:
|
async def get_current_user(self, token: str) -> object:
|
||||||
"""
|
"""
|
||||||
@@ -310,37 +374,46 @@ class CleverThisServiceAdapter:
|
|||||||
_amq_response: Any | None = None
|
_amq_response: Any | None = None
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f' [*] on_message: _auth_user={_auth_user}, require_authenticated_user={self.require_authenticated_user}')
|
f" [*] on_message: _auth_user={_auth_user}, require_authenticated_user={self.require_authenticated_user}"
|
||||||
|
)
|
||||||
if self.require_authenticated_user:
|
if self.require_authenticated_user:
|
||||||
_auth: str = message.headers.get('Authorization', '')
|
_auth: str = message.headers.get("Authorization", "")
|
||||||
logging.info(f"Auth: {_auth}")
|
logging.info(f"Auth: {_auth}")
|
||||||
if isinstance(_auth, List):
|
if isinstance(_auth, List):
|
||||||
_auth = _auth[0]
|
_auth = _auth[0]
|
||||||
if 'Bearer' in _auth:
|
if "Bearer" in _auth:
|
||||||
try:
|
try:
|
||||||
token = _auth.split(' ')[1]
|
token = _auth.split(" ")[1]
|
||||||
logging.warn(f'Token: {token}')
|
logging.warn(f"Token: {token}")
|
||||||
_auth_user = await self.get_current_user(token)
|
_auth_user = await self.get_current_user(token)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logging_error(f"on_message: Error getting user: {error}")
|
logging_error(f"on_message: Error getting user: {error}")
|
||||||
|
|
||||||
if _auth_user or not self.require_authenticated_user or message.path.endswith('/login'):
|
if (
|
||||||
|
_auth_user
|
||||||
|
or not self.require_authenticated_user
|
||||||
|
or message.path.endswith("/login")
|
||||||
|
):
|
||||||
method = message.method.lower()
|
method = message.method.lower()
|
||||||
if method == 'get':
|
if method == "get":
|
||||||
_amq_response = await self.get(message, _auth_user)
|
_amq_response = await self.get(message, _auth_user)
|
||||||
elif method == 'put':
|
elif method == "put":
|
||||||
_amq_response = await self.put(message, _auth_user)
|
_amq_response = await self.put(message, _auth_user)
|
||||||
elif method == 'post':
|
elif method == "post":
|
||||||
_amq_response = await self.post(message, _auth_user)
|
_amq_response = await self.post(message, _auth_user)
|
||||||
elif method == 'head':
|
elif method == "head":
|
||||||
_amq_response = await self.head(message, _auth_user)
|
_amq_response = await self.head(message, _auth_user)
|
||||||
elif method == 'delete':
|
elif method == "delete":
|
||||||
_amq_response = await self.delete(message, _auth_user)
|
_amq_response = await self.delete(message, _auth_user)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unexpected HTTP method: {message.method}")
|
raise ValueError(f"Unexpected HTTP method: {message.method}")
|
||||||
else:
|
else:
|
||||||
_amq_response = DataResponseFactory.of(message.id, 401, "text/plain", b"Unauthorized", message.trace_info)
|
_amq_response = DataResponseFactory.of(
|
||||||
logging.info(f" [x] ALL DONE in on_message id:{message.id}, resp code:{_amq_response.response_code}")
|
message.id, 401, "text/plain", b"Unauthorized", message.trace_info
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
f" [x] ALL DONE in on_message id:{message.id}, resp code:{_amq_response.response_code}"
|
||||||
|
)
|
||||||
return _amq_response
|
return _amq_response
|
||||||
|
|
||||||
# ==================================================================================================
|
# ==================================================================================================
|
||||||
@@ -359,14 +432,25 @@ class CleverThisServiceAdapter:
|
|||||||
# unmatched path - try to invoke the service directly on this path
|
# unmatched path - try to invoke the service directly on this path
|
||||||
url = f"http://{self.service_host}:{self.service_port}{message.path}"
|
url = f"http://{self.service_host}:{self.service_port}{message.path}"
|
||||||
headers = {**message.headers, **message.trace_info}
|
headers = {**message.headers, **message.trace_info}
|
||||||
logging.info(f" [*] SEND HEADERS out: {', '.join(f'{k}={v}' for k, v in headers.items())}, REQ={url}")
|
logging.info(
|
||||||
|
f" [*] SEND HEADERS out: {', '.join(f'{k}={v}' for k, v in headers.items())}, REQ={url}"
|
||||||
|
)
|
||||||
if self.session:
|
if self.session:
|
||||||
_http_resp: ClientResponse = await self.session.get(url, headers=headers)
|
_http_resp: ClientResponse = await self.session.get(url, headers=headers)
|
||||||
return DataResponseFactory.of(message.id, _http_resp.status, _http_resp.content_type,
|
return DataResponseFactory.of(
|
||||||
await _http_resp.read(),
|
message.id,
|
||||||
message.trace_info)
|
_http_resp.status,
|
||||||
return DataResponseFactory.of(message.id, 404, "text/plain",
|
_http_resp.content_type,
|
||||||
str("Destination location does not exists").encode('utf-8'), message.trace_info)
|
await _http_resp.read(),
|
||||||
|
message.trace_info,
|
||||||
|
)
|
||||||
|
return DataResponseFactory.of(
|
||||||
|
message.id,
|
||||||
|
404,
|
||||||
|
"text/plain",
|
||||||
|
str("Destination location does not exists").encode("utf-8"),
|
||||||
|
message.trace_info,
|
||||||
|
)
|
||||||
|
|
||||||
async def put(self, message: DataMessage, auth_user: Any) -> DataResponse:
|
async def put(self, message: DataMessage, auth_user: Any) -> DataResponse:
|
||||||
"""
|
"""
|
||||||
@@ -383,7 +467,7 @@ class CleverThisServiceAdapter:
|
|||||||
data=message.body,
|
data=message.body,
|
||||||
headers={**message.headers, **message.trace_info},
|
headers={**message.headers, **message.trace_info},
|
||||||
),
|
),
|
||||||
auth_user
|
auth_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def post(self, message: DataMessage, auth_user: Any) -> DataResponse:
|
async def post(self, message: DataMessage, auth_user: Any) -> DataResponse:
|
||||||
@@ -401,10 +485,12 @@ class CleverThisServiceAdapter:
|
|||||||
data=message.body,
|
data=message.body,
|
||||||
headers={**message.headers, **message.trace_info},
|
headers={**message.headers, **message.trace_info},
|
||||||
),
|
),
|
||||||
auth_user
|
auth_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_possible_form(self, message: DataMessage, request_coroutine, auth_user: Any) -> DataResponse:
|
async def handle_possible_form(
|
||||||
|
self, message: DataMessage, request_coroutine, auth_user: Any
|
||||||
|
) -> DataResponse:
|
||||||
_args = parse_request_body(message)
|
_args = parse_request_body(message)
|
||||||
return await request_coroutine
|
return await request_coroutine
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ from typing import Dict, List
|
|||||||
|
|
||||||
from amqp.model.model import DataMessage
|
from amqp.model.model import DataMessage
|
||||||
from amqp.model.snowflake_id import SnowflakeId
|
from amqp.model.snowflake_id import SnowflakeId
|
||||||
from amqp.adapter.serializer import map_with_list_as_string, map_as_string, parse_map_list_string, parse_map_string
|
from amqp.adapter.serializer import (
|
||||||
|
map_with_list_as_string,
|
||||||
|
map_as_string,
|
||||||
|
parse_map_list_string,
|
||||||
|
parse_map_string,
|
||||||
|
)
|
||||||
from amqp.router.utils import sanitize_as_rabbitmq_name
|
from amqp.router.utils import sanitize_as_rabbitmq_name
|
||||||
from amqp.adapter.file_downloader import download_buffer
|
from amqp.adapter.file_downloader import download_buffer
|
||||||
|
|
||||||
@@ -65,19 +70,24 @@ class DataMessageFactory:
|
|||||||
self.snowflake_sequence = 0
|
self.snowflake_sequence = 0
|
||||||
self.snowflake_last_timestamp = timestamp
|
self.snowflake_last_timestamp = timestamp
|
||||||
|
|
||||||
time = (timestamp - 1000 * int(DataMessageFactory.snowflake_epoch))
|
time = timestamp - 1000 * int(DataMessageFactory.snowflake_epoch)
|
||||||
return SnowflakeId(
|
return SnowflakeId(
|
||||||
time >> (64 - self.SEQUENCE_BITS - self.MACHINE_ID_BITS),
|
time >> (64 - self.SEQUENCE_BITS - self.MACHINE_ID_BITS),
|
||||||
((time << (self.SEQUENCE_BITS + self.MACHINE_ID_BITS)) | self.machine_id | self.snowflake_sequence) & 0xFFFFFFFFFFFFFFFF
|
(
|
||||||
|
(time << (self.SEQUENCE_BITS + self.MACHINE_ID_BITS))
|
||||||
|
| self.machine_id
|
||||||
|
| self.snowflake_sequence
|
||||||
|
)
|
||||||
|
& 0xFFFFFFFFFFFFFFFF,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_request_message(
|
def create_request_message(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
domain: str,
|
domain: str,
|
||||||
path: str,
|
path: str,
|
||||||
headers: Dict[str, List[str]],
|
headers: Dict[str, List[str]],
|
||||||
message,
|
message,
|
||||||
) -> DataMessage:
|
) -> DataMessage:
|
||||||
"""
|
"""
|
||||||
Encapsulate / hide the system level inputs, create the AMQ message from business relevant input
|
Encapsulate / hide the system level inputs, create the AMQ message from business relevant input
|
||||||
@@ -89,7 +99,9 @@ class DataMessageFactory:
|
|||||||
:return: DataMessage object
|
:return: DataMessage object
|
||||||
"""
|
"""
|
||||||
serializable_headers = headers.copy() if headers else {}
|
serializable_headers = headers.copy() if headers else {}
|
||||||
payload = base64.b64encode(message.encode('utf-8') if isinstance(message, str) else message)
|
payload = base64.b64encode(
|
||||||
|
message.encode("utf-8") if isinstance(message, str) else message
|
||||||
|
)
|
||||||
trace_info = {}
|
trace_info = {}
|
||||||
return DataMessage(
|
return DataMessage(
|
||||||
self.MAGIC_BYTE_HTTP_REQUEST,
|
self.MAGIC_BYTE_HTTP_REQUEST,
|
||||||
@@ -119,8 +131,12 @@ class DataMessageFactory:
|
|||||||
:return: the message timestamp
|
:return: the message timestamp
|
||||||
"""
|
"""
|
||||||
bits = _id.get_bits()
|
bits = _id.get_bits()
|
||||||
low = bits[0] >> (DataMessageFactory.SEQUENCE_BITS + DataMessageFactory.MACHINE_ID_BITS)
|
low = bits[0] >> (
|
||||||
high = bits[1] << (64 - DataMessageFactory.SEQUENCE_BITS - DataMessageFactory.MACHINE_ID_BITS)
|
DataMessageFactory.SEQUENCE_BITS + DataMessageFactory.MACHINE_ID_BITS
|
||||||
|
)
|
||||||
|
high = bits[1] << (
|
||||||
|
64 - DataMessageFactory.SEQUENCE_BITS - DataMessageFactory.MACHINE_ID_BITS
|
||||||
|
)
|
||||||
return (high | low) + DataMessageFactory.snowflake_epoch * 1000
|
return (high | low) + DataMessageFactory.snowflake_epoch * 1000
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -152,13 +168,13 @@ class DataMessageFactory:
|
|||||||
id_bytes = str(message.id).encode()
|
id_bytes = str(message.id).encode()
|
||||||
prolog = (
|
prolog = (
|
||||||
f"|m={message.method}|d={message.domain}|p={message.path}|h={{{map_with_list_as_string(message.headers)}}}|t={{{map_as_string(message.trace_info)}}}|b="
|
f"|m={message.method}|d={message.domain}|p={message.path}|h={{{map_with_list_as_string(message.headers)}}}|t={{{map_as_string(message.trace_info)}}}|b="
|
||||||
).encode('utf-8')
|
).encode("utf-8")
|
||||||
header_length = 1 + len(id_bytes) + len(prolog)
|
header_length = 1 + len(id_bytes) + len(prolog)
|
||||||
prolog_len = header_length.to_bytes(2, byteorder='big')
|
prolog_len = header_length.to_bytes(2, byteorder="big")
|
||||||
msg_length = 2 + header_length + len(message.base64_body)
|
msg_length = 2 + header_length + len(message.base64_body)
|
||||||
with BytesIO(bytearray(msg_length)) as baos:
|
with BytesIO(bytearray(msg_length)) as baos:
|
||||||
baos.write(prolog_len)
|
baos.write(prolog_len)
|
||||||
baos.write(DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST.encode('utf-8'))
|
baos.write(DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST.encode("utf-8"))
|
||||||
baos.write(id_bytes)
|
baos.write(id_bytes)
|
||||||
baos.write(prolog)
|
baos.write(prolog)
|
||||||
baos.write(message.base64_body)
|
baos.write(message.base64_body)
|
||||||
@@ -172,7 +188,7 @@ class DataMessageFactory:
|
|||||||
:return: DataMessage instance
|
:return: DataMessage instance
|
||||||
"""
|
"""
|
||||||
prolog_len = (input_bytes[0] << 8) + input_bytes[1]
|
prolog_len = (input_bytes[0] << 8) + input_bytes[1]
|
||||||
metadata = input_bytes[2:prolog_len + 2].decode()
|
metadata = input_bytes[2 : prolog_len + 2].decode()
|
||||||
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
f"{DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST}"
|
f"{DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST}"
|
||||||
@@ -197,7 +213,7 @@ class DataMessageFactory:
|
|||||||
path = match.group(4)
|
path = match.group(4)
|
||||||
headers_str = match.group(5)
|
headers_str = match.group(5)
|
||||||
trace_info_str = match.group(6)
|
trace_info_str = match.group(6)
|
||||||
body_b64 = input_bytes[prolog_len + 2:]
|
body_b64 = input_bytes[prolog_len + 2 :]
|
||||||
|
|
||||||
headers = parse_map_list_string(headers_str)
|
headers = parse_map_list_string(headers_str)
|
||||||
trace_info = parse_map_string(trace_info_str)
|
trace_info = parse_map_string(trace_info_str)
|
||||||
@@ -223,9 +239,11 @@ class DataMessageFactory:
|
|||||||
"""
|
"""
|
||||||
# async implementation
|
# async implementation
|
||||||
message_data = json.loads(stream.decode())
|
message_data = json.loads(stream.decode())
|
||||||
req_info = message_data.get('req-info', '')
|
req_info = message_data.get("req-info", "")
|
||||||
if req_info:
|
if req_info:
|
||||||
(body, eof) = await download_buffer(loop=loop, rabbitmq_url=rabbitmq_url, queue_name=req_info)
|
(body, eof) = await download_buffer(
|
||||||
|
loop=loop, rabbitmq_url=rabbitmq_url, queue_name=req_info
|
||||||
|
)
|
||||||
if eof == 1:
|
if eof == 1:
|
||||||
return DataMessageFactory.from_bytes(body)
|
return DataMessageFactory.from_bytes(body)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ class DataResponseFactory:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def of(id: SnowflakeId, response_code: int, content_type: str, body: bytes, trace_info: dict[str,str]) -> DataResponse:
|
def of(
|
||||||
|
id: SnowflakeId,
|
||||||
|
response_code: int,
|
||||||
|
content_type: str,
|
||||||
|
body: bytes,
|
||||||
|
trace_info: dict[str, str],
|
||||||
|
) -> DataResponse:
|
||||||
"""
|
"""
|
||||||
Create the DataResponse message supplying all key values
|
Create the DataResponse message supplying all key values
|
||||||
:param id: SnowflakeID
|
:param id: SnowflakeID
|
||||||
@@ -42,7 +48,7 @@ class DataResponseFactory:
|
|||||||
response=body,
|
response=body,
|
||||||
error=None,
|
error=None,
|
||||||
error_cause=None,
|
error_cause=None,
|
||||||
trace_info=trace_info
|
trace_info=trace_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -52,16 +58,16 @@ class DataResponseFactory:
|
|||||||
:param response: Object to serialize
|
:param response: Object to serialize
|
||||||
:return: byte array as serialized message
|
:return: byte array as serialized message
|
||||||
"""
|
"""
|
||||||
id_bytes = response.id.encode('utf-8')
|
id_bytes = response.id.encode("utf-8")
|
||||||
prolog = (
|
prolog = (
|
||||||
f"|r={response.response_code}|c={response.content_type}|e={response.error}|f={response.error_cause}|t={{{map_as_string(response.trace_info)}}}|b="
|
f"|r={response.response_code}|c={response.content_type}|e={response.error}|f={response.error_cause}|t={{{map_as_string(response.trace_info)}}}|b="
|
||||||
).encode('utf-8')
|
).encode("utf-8")
|
||||||
header_length = 1 + len(id_bytes) + len(prolog)
|
header_length = 1 + len(id_bytes) + len(prolog)
|
||||||
prolog_len = header_length.to_bytes(2, byteorder='big')
|
prolog_len = header_length.to_bytes(2, byteorder="big")
|
||||||
msg_length = 2 + header_length + len(response.response)
|
msg_length = 2 + header_length + len(response.response)
|
||||||
with BytesIO(bytearray(msg_length)) as baos:
|
with BytesIO(bytearray(msg_length)) as baos:
|
||||||
baos.write(prolog_len)
|
baos.write(prolog_len)
|
||||||
baos.write(DataResponseFactory.MAGIC_BYTE_HTTP_REQUEST.encode('utf-8'))
|
baos.write(DataResponseFactory.MAGIC_BYTE_HTTP_REQUEST.encode("utf-8"))
|
||||||
baos.write(id_bytes)
|
baos.write(id_bytes)
|
||||||
baos.write(prolog)
|
baos.write(prolog)
|
||||||
baos.write(base64.b64encode(response.response))
|
baos.write(base64.b64encode(response.response))
|
||||||
@@ -75,7 +81,7 @@ class DataResponseFactory:
|
|||||||
:return: DataResponse object
|
:return: DataResponse object
|
||||||
"""
|
"""
|
||||||
prolog_len = (input_bytes[0] << 8) + input_bytes[1]
|
prolog_len = (input_bytes[0] << 8) + input_bytes[1]
|
||||||
metadata = input_bytes[2:prolog_len + 2].decode()
|
metadata = input_bytes[2 : prolog_len + 2].decode()
|
||||||
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
f"{DataResponseFactory.MAGIC_BYTE_HTTP_REQUEST}"
|
f"{DataResponseFactory.MAGIC_BYTE_HTTP_REQUEST}"
|
||||||
@@ -100,7 +106,7 @@ class DataResponseFactory:
|
|||||||
error = match.group(4)
|
error = match.group(4)
|
||||||
error_cause = match.group(5)
|
error_cause = match.group(5)
|
||||||
trace_info_str = match.group(6)
|
trace_info_str = match.group(6)
|
||||||
body_b64 = input_bytes[prolog_len + 2:]
|
body_b64 = input_bytes[prolog_len + 2 :]
|
||||||
|
|
||||||
trace_info = parse_map_string(trace_info_str)
|
trace_info = parse_map_string(trace_info_str)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ def ensure_directory_exists(filepath) -> str:
|
|||||||
version += 1
|
version += 1
|
||||||
|
|
||||||
|
|
||||||
async def download_file(loop, rabbitmq_url, file_info, message_data, output_dir) -> (int, int):
|
async def download_file(
|
||||||
|
loop, rabbitmq_url, file_info, message_data, output_dir
|
||||||
|
) -> (int, int):
|
||||||
"""
|
"""
|
||||||
Asynchronously downloads a file from RabbitMQ based on the provided file information.
|
Asynchronously downloads a file from RabbitMQ based on the provided file information.
|
||||||
takes the file_info and message_data and output_dir, creates queue consumer and waits for the file to be downloaded
|
takes the file_info and message_data and output_dir, creates queue consumer and waits for the file to be downloaded
|
||||||
@@ -64,17 +66,23 @@ async def download_file(loop, rabbitmq_url, file_info, message_data, output_dir)
|
|||||||
output_dir (str): The directory where the file should be saved.
|
output_dir (str): The directory where the file should be saved.
|
||||||
"""
|
"""
|
||||||
global download_locations
|
global download_locations
|
||||||
filename = file_info['filename']
|
filename = file_info["filename"]
|
||||||
size = file_info['size']
|
size = file_info["size"]
|
||||||
stream_name = file_info['streamName']
|
stream_name = file_info["streamName"]
|
||||||
queue_name = file_info['queue_name']
|
queue_name = file_info["queue_name"]
|
||||||
filepath = os.path.join(output_dir, filename)
|
filepath = os.path.join(output_dir, filename)
|
||||||
filepath = ensure_directory_exists(filepath) # if filename exists, create a new (next) version
|
filepath = ensure_directory_exists(
|
||||||
download_locations[file_info['key']] = filepath
|
filepath
|
||||||
filename = os.path.basename(filepath) # if a new version was created, ensure to use that new version
|
) # if filename exists, create a new (next) version
|
||||||
file_info['filename'] = filename
|
download_locations[file_info["key"]] = filepath
|
||||||
|
filename = os.path.basename(
|
||||||
|
filepath
|
||||||
|
) # if a new version was created, ensure to use that new version
|
||||||
|
file_info["filename"] = filename
|
||||||
|
|
||||||
logging.info(f"Downloading file: {filename} (size: {size} bytes) from stream: {stream_name}:{queue_name} to {filepath}")
|
logging.info(
|
||||||
|
f"Downloading file: {filename} (size: {size} bytes) from stream: {stream_name}:{queue_name} to {filepath}"
|
||||||
|
)
|
||||||
|
|
||||||
async def wrap_rabbit_mq_calls() -> tuple[int, int] | None:
|
async def wrap_rabbit_mq_calls() -> tuple[int, int] | None:
|
||||||
connection = await connect_robust(rabbitmq_url, loop=loop)
|
connection = await connect_robust(rabbitmq_url, loop=loop)
|
||||||
@@ -87,10 +95,10 @@ async def download_file(loop, rabbitmq_url, file_info, message_data, output_dir)
|
|||||||
async with queue.iterator() as queue_iter:
|
async with queue.iterator() as queue_iter:
|
||||||
async for message in queue_iter:
|
async for message in queue_iter:
|
||||||
async with message.process():
|
async with message.process():
|
||||||
with open(filepath, 'ab') as f:
|
with open(filepath, "ab") as f:
|
||||||
f.write(message.body)
|
f.write(message.body)
|
||||||
_file_size = _file_size + message.body_size
|
_file_size = _file_size + message.body_size
|
||||||
_eof = int(message.headers.get('eof', END_OF_FILE))
|
_eof = int(message.headers.get("eof", END_OF_FILE))
|
||||||
if _eof != IN_PROGRESS:
|
if _eof != IN_PROGRESS:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -101,7 +109,9 @@ async def download_file(loop, rabbitmq_url, file_info, message_data, output_dir)
|
|||||||
return _file_size, _eof
|
return _file_size, _eof
|
||||||
|
|
||||||
if queue_name:
|
if queue_name:
|
||||||
_future: Future = asyncio.run_coroutine_threadsafe(wrap_rabbit_mq_calls(), loop=loop)
|
_future: Future = asyncio.run_coroutine_threadsafe(
|
||||||
|
wrap_rabbit_mq_calls(), loop=loop
|
||||||
|
)
|
||||||
while not _future.done():
|
while not _future.done():
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
_local_file_size, _local_eof = _future.result()
|
_local_file_size, _local_eof = _future.result()
|
||||||
@@ -109,6 +119,7 @@ async def download_file(loop, rabbitmq_url, file_info, message_data, output_dir)
|
|||||||
|
|
||||||
return 0, CANCELLED
|
return 0, CANCELLED
|
||||||
|
|
||||||
|
|
||||||
async def download_buffer(loop, rabbitmq_url, queue_name) -> (bytearray, int):
|
async def download_buffer(loop, rabbitmq_url, queue_name) -> (bytearray, int):
|
||||||
"""
|
"""
|
||||||
Asynchronously downloads a bytearay from RabbitMQ based on the provided file information.
|
Asynchronously downloads a bytearay from RabbitMQ based on the provided file information.
|
||||||
@@ -120,6 +131,7 @@ async def download_buffer(loop, rabbitmq_url, queue_name) -> (bytearray, int):
|
|||||||
"""
|
"""
|
||||||
logging.info(f"Downloading buffer from stream: {queue_name}")
|
logging.info(f"Downloading buffer from stream: {queue_name}")
|
||||||
if queue_name:
|
if queue_name:
|
||||||
|
|
||||||
async def wrap_rabbit_mq_calls() -> tuple[bytearray, int]:
|
async def wrap_rabbit_mq_calls() -> tuple[bytearray, int]:
|
||||||
_connection = await connect_robust(rabbitmq_url, loop=loop)
|
_connection = await connect_robust(rabbitmq_url, loop=loop)
|
||||||
_eof = IN_PROGRESS
|
_eof = IN_PROGRESS
|
||||||
@@ -127,12 +139,14 @@ async def download_buffer(loop, rabbitmq_url, queue_name) -> (bytearray, int):
|
|||||||
try:
|
try:
|
||||||
channel: AbstractRobustChannel = await _connection.channel()
|
channel: AbstractRobustChannel = await _connection.channel()
|
||||||
queue: AbstractQueue = await channel.get_queue(queue_name, ensure=False)
|
queue: AbstractQueue = await channel.get_queue(queue_name, ensure=False)
|
||||||
logging.info(f"Awaiting all buffer chunks being downloaded for {queue_name}")
|
logging.info(
|
||||||
|
f"Awaiting all buffer chunks being downloaded for {queue_name}"
|
||||||
|
)
|
||||||
async with queue.iterator() as queue_iter:
|
async with queue.iterator() as queue_iter:
|
||||||
async for message in queue_iter:
|
async for message in queue_iter:
|
||||||
async with message.process():
|
async with message.process():
|
||||||
_res.extend(message.body)
|
_res.extend(message.body)
|
||||||
_eof = message.headers.get('eof', END_OF_FILE)
|
_eof = message.headers.get("eof", END_OF_FILE)
|
||||||
if _eof != IN_PROGRESS:
|
if _eof != IN_PROGRESS:
|
||||||
break
|
break
|
||||||
logging.info(f"Finished downloading buffer {queue_name}")
|
logging.info(f"Finished downloading buffer {queue_name}")
|
||||||
@@ -142,7 +156,9 @@ async def download_buffer(loop, rabbitmq_url, queue_name) -> (bytearray, int):
|
|||||||
await _connection.close()
|
await _connection.close()
|
||||||
return _res, _eof
|
return _res, _eof
|
||||||
|
|
||||||
_future: Future = asyncio.run_coroutine_threadsafe(wrap_rabbit_mq_calls(), loop=loop)
|
_future: Future = asyncio.run_coroutine_threadsafe(
|
||||||
|
wrap_rabbit_mq_calls(), loop=loop
|
||||||
|
)
|
||||||
while not _future.done():
|
while not _future.done():
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
_local_res, _local_eof = _future.result()
|
_local_res, _local_eof = _future.result()
|
||||||
@@ -155,7 +171,7 @@ async def on_message(
|
|||||||
json_data: bytes,
|
json_data: bytes,
|
||||||
rabbitmq_url: str,
|
rabbitmq_url: str,
|
||||||
loop,
|
loop,
|
||||||
output_dir="downloaded_files"
|
output_dir="downloaded_files",
|
||||||
) -> defaultdict:
|
) -> defaultdict:
|
||||||
"""
|
"""
|
||||||
Asynchronous callback function for handling messages from the 'amq-cleverswarm' queue.
|
Asynchronous callback function for handling messages from the 'amq-cleverswarm' queue.
|
||||||
@@ -170,35 +186,47 @@ async def on_message(
|
|||||||
_amq_message_data = json.loads(json_data.decode())
|
_amq_message_data = json.loads(json_data.decode())
|
||||||
|
|
||||||
files_to_download = []
|
files_to_download = []
|
||||||
for _key, _file_type in _amq_message_data['files'].items():
|
for _key, _file_type in _amq_message_data["files"].items():
|
||||||
if len(_file_type) > 0:
|
if len(_file_type) > 0:
|
||||||
files_info = _file_type[0]
|
files_info = _file_type[0]
|
||||||
stream_name = files_info['streamName']
|
stream_name = files_info["streamName"]
|
||||||
files_info['queue_name'] = _message_data.get(stream_name, "") if stream_name else ""
|
files_info["queue_name"] = (
|
||||||
files_info['key'] = _key
|
_message_data.get(stream_name, "") if stream_name else ""
|
||||||
|
)
|
||||||
|
files_info["key"] = _key
|
||||||
files_to_download.append(files_info)
|
files_to_download.append(files_info)
|
||||||
|
|
||||||
if not files_to_download:
|
if not files_to_download:
|
||||||
logging.info(" [%s] No files to download in this message.", message.delivery_tag)
|
logging.info(
|
||||||
|
" [%s] No files to download in this message.", message.delivery_tag
|
||||||
|
)
|
||||||
# await message.ack()
|
# await message.ack()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create a task for each file download
|
# Create a task for each file download
|
||||||
_tasks = []
|
_tasks = []
|
||||||
for file_info in files_to_download:
|
for file_info in files_to_download:
|
||||||
logging.info(f" [{message.delivery_tag}] about to create task for {file_info}")
|
logging.info(
|
||||||
_task = asyncio.create_task(download_file(loop, rabbitmq_url, file_info, _message_data, output_dir))
|
f" [{message.delivery_tag}] about to create task for {file_info}"
|
||||||
|
)
|
||||||
|
_task = asyncio.create_task(
|
||||||
|
download_file(loop, rabbitmq_url, file_info, _message_data, output_dir)
|
||||||
|
)
|
||||||
_tasks.append(_task)
|
_tasks.append(_task)
|
||||||
logging.info(f" [{message.delivery_tag}] Waiting for all files to download...gather(*tasks)")
|
logging.info(
|
||||||
await asyncio.gather(*_tasks) # Wait for all downloads to complete
|
f" [{message.delivery_tag}] Waiting for all files to download...gather(*tasks)"
|
||||||
|
)
|
||||||
|
await asyncio.gather(*_tasks) # Wait for all downloads to complete
|
||||||
|
|
||||||
# After all files are downloaded, acknowledge the original message
|
# After all files are downloaded, acknowledge the original message
|
||||||
logging.info(f"All files downloaded. d-tag: {message.delivery_tag}")
|
logging.info(f"All files downloaded. d-tag: {message.delivery_tag}")
|
||||||
#logging.info(f"All files downloaded. Acknowledging the main message d-tag: {message.delivery_tag}")
|
# logging.info(f"All files downloaded. Acknowledging the main message d-tag: {message.delivery_tag}")
|
||||||
#await message.ack()
|
# await message.ack()
|
||||||
#logging.info(f"Message d-tag: {message.delivery_tag} acknowledged.")
|
# logging.info(f"Message d-tag: {message.delivery_tag} acknowledged.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(f"Building DataMessage: {e}")
|
logging_error(f"Building DataMessage: {e}")
|
||||||
asyncio.run_coroutine_threadsafe(message.ack(), loop=loop) # ACK -> ignore the message (do not redeliver)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
message.ack(), loop=loop
|
||||||
|
) # ACK -> ignore the message (do not redeliver)
|
||||||
|
|
||||||
return download_locations
|
return download_locations
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ async def publish_file_to_rabbitmq(
|
|||||||
routing_key: str,
|
routing_key: str,
|
||||||
loop: AbstractEventLoop,
|
loop: AbstractEventLoop,
|
||||||
max_chunk_size: int = 2**20, # 1MB default chunk size
|
max_chunk_size: int = 2**20, # 1MB default chunk size
|
||||||
content_type: str = 'application/octet-stream', # Default content type
|
content_type: str = "application/octet-stream", # Default content type
|
||||||
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
|
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
|
||||||
) -> tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
@@ -45,7 +45,7 @@ async def publish_file_to_rabbitmq(
|
|||||||
raise FileNotFoundError(f"File not found: {file_path}")
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
_file_size = os.path.getsize(file_path)
|
_file_size = os.path.getsize(file_path)
|
||||||
|
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
_offset = 0
|
_offset = 0
|
||||||
while True:
|
while True:
|
||||||
_chunk = f.read(max_chunk_size)
|
_chunk = f.read(max_chunk_size)
|
||||||
@@ -57,22 +57,36 @@ async def publish_file_to_rabbitmq(
|
|||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
delivery_mode=delivery_mode,
|
delivery_mode=delivery_mode,
|
||||||
headers={
|
headers={
|
||||||
'file_name': file_path.name,
|
"file_name": file_path.name,
|
||||||
'pos': _offset,
|
"pos": _offset,
|
||||||
'total_size': _file_size,
|
"total_size": _file_size,
|
||||||
'chunk_size': len(_chunk),
|
"chunk_size": len(_chunk),
|
||||||
'eof': END_OF_FILE if max_chunk_size + _offset >= _file_size else IN_PROGRESS
|
"eof": (
|
||||||
}
|
END_OF_FILE
|
||||||
|
if max_chunk_size + _offset >= _file_size
|
||||||
|
else IN_PROGRESS
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logging.debug(
|
||||||
|
f"Going to Publish chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}"
|
||||||
|
)
|
||||||
|
_future = asyncio.run_coroutine_threadsafe(
|
||||||
|
exchange.publish(message, routing_key=routing_key), loop
|
||||||
)
|
)
|
||||||
logging.debug(f"Going to Publish chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}")
|
|
||||||
_future = asyncio.run_coroutine_threadsafe(exchange.publish(message, routing_key=routing_key), loop)
|
|
||||||
while not _future.done():
|
while not _future.done():
|
||||||
# sleep allows coroutine to execute. must wait for completion to ensure all chunks written in right order
|
# sleep allows coroutine to execute. must wait for completion to ensure all chunks written in right order
|
||||||
await asyncio.sleep(0.02)
|
await asyncio.sleep(0.02)
|
||||||
logging.debug(f"Waiting for chunk {_offset}/{_file_size} future= {_future}")
|
logging.debug(
|
||||||
|
f"Waiting for chunk {_offset}/{_file_size} future= {_future}"
|
||||||
|
)
|
||||||
|
|
||||||
_offset += len(_chunk)
|
_offset += len(_chunk)
|
||||||
logging.debug(f"Published chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}")
|
logging.debug(
|
||||||
|
f"Published chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}"
|
||||||
|
)
|
||||||
|
|
||||||
logging_debug(f"File {file_path} published to RabbitMQ exchange {exchange.name}, queue {routing_key}")
|
logging_debug(
|
||||||
|
f"File {file_path} published to RabbitMQ exchange {exchange.name}, queue {routing_key}"
|
||||||
|
)
|
||||||
return routing_key, _file_size
|
return routing_key, _file_size
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ def logging_error(msg: str, *args) -> None:
|
|||||||
_exec_info = sys.exc_info()[2]
|
_exec_info = sys.exc_info()[2]
|
||||||
if _exec_info:
|
if _exec_info:
|
||||||
_tb = traceback.extract_tb(sys.exc_info()[2])[-1]
|
_tb = traceback.extract_tb(sys.exc_info()[2])[-1]
|
||||||
logging.error(f" [E] '{msg}' ---> '{_tb.filename}': {_tb.lineno}, func: '{_tb.name}'", *args)
|
logging.error(
|
||||||
|
f" [E] '{msg}' ---> '{_tb.filename}': {_tb.lineno}, func: '{_tb.name}'",
|
||||||
|
*args,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.error(f" [E] {msg}", *args)
|
logging.error(f" [E] {msg}", *args)
|
||||||
|
|
||||||
@@ -28,6 +31,9 @@ def logging_debug(msg: str, *args) -> None:
|
|||||||
_exec_info = sys.exc_info()[2]
|
_exec_info = sys.exc_info()[2]
|
||||||
if _exec_info:
|
if _exec_info:
|
||||||
_tb = traceback.extract_tb(sys.exc_info()[2])[-1]
|
_tb = traceback.extract_tb(sys.exc_info()[2])[-1]
|
||||||
logging.debug(f" [DBG] '{msg}' ---> '{_tb.filename}': {_tb.lineno}, func: '{_tb.name}'", *args)
|
logging.debug(
|
||||||
|
f" [DBG] '{msg}' ---> '{_tb.filename}': {_tb.lineno}, func: '{_tb.name}'",
|
||||||
|
*args,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug(f" [DBG] {msg}", *args)
|
logging.debug(f" [DBG] {msg}", *args)
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ def process_form_data(json_input) -> dict:
|
|||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
# Process form data - take first element of each list
|
# Process form data - take first element of each list
|
||||||
for key, value in data['form'].items():
|
for key, value in data["form"].items():
|
||||||
if value: # Only process if list is not empty
|
if value: # Only process if list is not empty
|
||||||
result[key] = value[0]
|
result[key] = value[0]
|
||||||
|
|
||||||
# Override with files data where available
|
# Override with files data where available
|
||||||
for key, value in data['files'].items():
|
for key, value in data["files"].items():
|
||||||
if value: # Only override if files data is non-empty
|
if value: # Only override if files data is non-empty
|
||||||
result[key] = value[0]
|
result[key] = value[0]
|
||||||
|
|
||||||
@@ -52,32 +52,45 @@ class CleverMultiPartParser:
|
|||||||
self.is_json = False
|
self.is_json = False
|
||||||
self.is_valid = True
|
self.is_valid = True
|
||||||
# Convert each list of strings to a single string joined by newline, then to bytes
|
# Convert each list of strings to a single string joined by newline, then to bytes
|
||||||
headers = {key: "\n".join(value).encode("utf-8") for key, value in message.headers.items()}
|
headers = {
|
||||||
|
key: "\n".join(value).encode("utf-8")
|
||||||
|
for key, value in message.headers.items()
|
||||||
|
}
|
||||||
content_type: str | bytes | None = headers.get("Content-Type")
|
content_type: str | bytes | None = headers.get("Content-Type")
|
||||||
content_type, params = multipart.parse_options_header(content_type)
|
content_type, params = multipart.parse_options_header(content_type)
|
||||||
if content_type.decode('utf-8') in ["application/octet-stream",
|
if content_type.decode("utf-8") in [
|
||||||
"multipart/form-data",
|
"application/octet-stream",
|
||||||
"application/x-www-form-urlencoded",
|
"multipart/form-data",
|
||||||
"application/x-url-encoded"]:
|
"application/x-www-form-urlencoded",
|
||||||
|
"application/x-url-encoded",
|
||||||
|
]:
|
||||||
try:
|
try:
|
||||||
if hasattr(message, 'extra_data') and isinstance(message.extra_data, dict) and len(message.extra_data) > 0:
|
if (
|
||||||
|
hasattr(message, "extra_data")
|
||||||
|
and isinstance(message.extra_data, dict)
|
||||||
|
and len(message.extra_data) > 0
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
self.form_data = json.loads(message.body())
|
self.form_data = json.loads(message.body())
|
||||||
self.is_json = True
|
self.is_json = True
|
||||||
if self.form_data['files'] and self.form_data['form']:
|
if self.form_data["files"] and self.form_data["form"]:
|
||||||
self.form_data = process_form_data(self.form_data)
|
self.form_data = process_form_data(self.form_data)
|
||||||
except Exception as jsonerror:
|
except Exception as jsonerror:
|
||||||
logging_error(f"Parsing JSON: {jsonerror}")
|
logging_error(f"Parsing JSON: {jsonerror}")
|
||||||
self.is_valid = False
|
self.is_valid = False
|
||||||
else:
|
else:
|
||||||
python_multipart.parse_form(headers, io.BytesIO(message.body()), self.on_field, self.on_file)
|
python_multipart.parse_form(
|
||||||
|
headers, io.BytesIO(message.body()), self.on_field, self.on_file
|
||||||
|
)
|
||||||
self.is_multipart = True
|
self.is_multipart = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(f"Error parsing multipart/form-data: {e}. Trying parsing as JSON.")
|
logging_error(
|
||||||
|
f"Error parsing multipart/form-data: {e}. Trying parsing as JSON."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.form_data = json.loads(message.body())
|
self.form_data = json.loads(message.body())
|
||||||
self.is_json = True
|
self.is_json = True
|
||||||
if self.form_data['files'] and self.form_data['form']:
|
if self.form_data["files"] and self.form_data["form"]:
|
||||||
self.form_data = process_form_data(self.form_data)
|
self.form_data = process_form_data(self.form_data)
|
||||||
except Exception as jsonerror:
|
except Exception as jsonerror:
|
||||||
logging_error(f"Parsing JSON: {jsonerror}")
|
logging_error(f"Parsing JSON: {jsonerror}")
|
||||||
@@ -87,11 +100,12 @@ class CleverMultiPartParser:
|
|||||||
|
|
||||||
def on_field(self, field: Field):
|
def on_field(self, field: Field):
|
||||||
logging_debug(field)
|
logging_debug(field)
|
||||||
self.form_data[field.field_name.decode('utf-8')] = field.value
|
self.form_data[field.field_name.decode("utf-8")] = field.value
|
||||||
logging_debug(self.form_data)
|
logging_debug(self.form_data)
|
||||||
|
|
||||||
def on_file(self, file: File):
|
def on_file(self, file: File):
|
||||||
logging_debug(file)
|
logging_debug(file)
|
||||||
self.form_data[file.field_name.decode('utf-8')] = \
|
self.form_data[file.field_name.decode("utf-8")] = UploadFile(
|
||||||
UploadFile(file.file_object, size=file.size, filename=file.file_name)
|
file.file_object, size=file.size, filename=file.file_name
|
||||||
|
)
|
||||||
logging_debug(self.form_data)
|
logging_debug(self.form_data)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ def deserialize_object(data: str) -> BaseModel:
|
|||||||
An R2RSerializable object.
|
An R2RSerializable object.
|
||||||
"""
|
"""
|
||||||
class_name, dict_str = data.split(":", 1)
|
class_name, dict_str = data.split(":", 1)
|
||||||
dict_str = dict_str.strip('{}')
|
dict_str = dict_str.strip("{}")
|
||||||
pairs = dict_str.split(",") if dict_str else []
|
pairs = dict_str.split(",") if dict_str else []
|
||||||
|
|
||||||
obj_dict: Dict[str, Any] = {}
|
obj_dict: Dict[str, Any] = {}
|
||||||
@@ -75,8 +75,8 @@ def deserialize_object(data: str) -> BaseModel:
|
|||||||
obj_dict[k] = _convert_value(v)
|
obj_dict[k] = _convert_value(v)
|
||||||
|
|
||||||
logging_debug(f"Deserialized object: {class_name}")
|
logging_debug(f"Deserialized object: {class_name}")
|
||||||
if '[' in class_name:
|
if "[" in class_name:
|
||||||
class_name = class_name.split('[')[0]
|
class_name = class_name.split("[")[0]
|
||||||
logging_debug(f"trying object: {class_name}")
|
logging_debug(f"trying object: {class_name}")
|
||||||
cls = globals().get(class_name)
|
cls = globals().get(class_name)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
@@ -89,7 +89,7 @@ def _convert_value(v: str) -> Any:
|
|||||||
"""
|
"""
|
||||||
Helper function to convert a string to its appropriate Python type.
|
Helper function to convert a string to its appropriate Python type.
|
||||||
"""
|
"""
|
||||||
if v == 'None':
|
if v == "None":
|
||||||
return None
|
return None
|
||||||
elif _is_uuid_string(v):
|
elif _is_uuid_string(v):
|
||||||
return uuid.UUID(v)
|
return uuid.UUID(v)
|
||||||
@@ -97,8 +97,13 @@ def _convert_value(v: str) -> Any:
|
|||||||
return datetime.fromisoformat(v)
|
return datetime.fromisoformat(v)
|
||||||
elif _is_int_string(v):
|
elif _is_int_string(v):
|
||||||
return int(v)
|
return int(v)
|
||||||
elif v.startswith("{") and v.endswith("}") and ":" in v and "{" not in v[1:-1] and "}" not in v[
|
elif (
|
||||||
1:-1]: # nested object
|
v.startswith("{")
|
||||||
|
and v.endswith("}")
|
||||||
|
and ":" in v
|
||||||
|
and "{" not in v[1:-1]
|
||||||
|
and "}" not in v[1:-1]
|
||||||
|
): # nested object
|
||||||
# check if it is a json dict
|
# check if it is a json dict
|
||||||
try:
|
try:
|
||||||
json.loads(v)
|
json.loads(v)
|
||||||
|
|||||||
+13
-13
@@ -8,16 +8,16 @@ from typing import List, Union, Dict
|
|||||||
|
|
||||||
|
|
||||||
def long_to_bytes(x: int) -> bytes:
|
def long_to_bytes(x: int) -> bytes:
|
||||||
return struct.pack('q', x)
|
return struct.pack("q", x)
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_long(b: bytes) -> int:
|
def bytes_to_long(b: bytes) -> int:
|
||||||
return struct.unpack('q', b)[0]
|
return struct.unpack("q", b)[0]
|
||||||
|
|
||||||
|
|
||||||
def read_long(bis: BytesIO) -> int:
|
def read_long(bis: BytesIO) -> int:
|
||||||
try:
|
try:
|
||||||
return struct.unpack('q', bis.read(8))[0]
|
return struct.unpack("q", bis.read(8))[0]
|
||||||
except IOError:
|
except IOError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -29,37 +29,37 @@ def object_or_list_as_string(value: Union[str, List[str]]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def map_as_string(map_: Dict[str, str]) -> str:
|
def map_as_string(map_: Dict[str, str]) -> str:
|
||||||
return ','.join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
|
return ",".join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
|
||||||
|
|
||||||
|
|
||||||
def map_with_list_as_string(map_: Dict[str, List[str]]) -> str:
|
def map_with_list_as_string(map_: Dict[str, List[str]]) -> str:
|
||||||
return ','.join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
|
return ",".join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
|
||||||
|
|
||||||
|
|
||||||
def parse_map_string(input_str: str) -> Dict[str, str]:
|
def parse_map_string(input_str: str) -> Dict[str, str]:
|
||||||
map_ = {}
|
map_ = {}
|
||||||
if input_str.startswith('{') and input_str.endswith('}'):
|
if input_str.startswith("{") and input_str.endswith("}"):
|
||||||
for entry in input_str[1:-1].split(','):
|
for entry in input_str[1:-1].split(","):
|
||||||
if len(entry) > 1:
|
if len(entry) > 1:
|
||||||
key, value = entry.split('=')
|
key, value = entry.split("=")
|
||||||
map_[key] = value
|
map_[key] = value
|
||||||
return map_
|
return map_
|
||||||
|
|
||||||
|
|
||||||
def csv_as_list(input_str: str) -> List[str]:
|
def csv_as_list(input_str: str) -> List[str]:
|
||||||
return [item.strip() for item in input_str.strip('[]').split(',')]
|
return [item.strip() for item in input_str.strip("[]").split(",")]
|
||||||
|
|
||||||
|
|
||||||
def add_to_map(map_: Dict[str, List[str]], token: str) -> None:
|
def add_to_map(map_: Dict[str, List[str]], token: str) -> None:
|
||||||
eq_idx = token.index('=')
|
eq_idx = token.index("=")
|
||||||
if eq_idx > 0:
|
if eq_idx > 0:
|
||||||
key = token[:eq_idx]
|
key = token[:eq_idx]
|
||||||
map_[key] = csv_as_list(token[eq_idx+1:])
|
map_[key] = csv_as_list(token[eq_idx + 1 :])
|
||||||
|
|
||||||
|
|
||||||
def parse_map_list_string(input_str: str) -> Dict[str, List[str]]:
|
def parse_map_list_string(input_str: str) -> Dict[str, List[str]]:
|
||||||
map_ = {}
|
map_ = {}
|
||||||
if input_str.startswith('{') and input_str.endswith('}'):
|
if input_str.startswith("{") and input_str.endswith("}"):
|
||||||
for token in input_str[1:-1].split('],'):
|
for token in input_str[1:-1].split("],"):
|
||||||
add_to_map(map_, token)
|
add_to_map(map_, token)
|
||||||
return map_
|
return map_
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ServiceMessageFactory:
|
|||||||
"""
|
"""
|
||||||
build/serialize/deserialize CleverMicro Service Message class
|
build/serialize/deserialize CleverMicro Service Message class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
self.process_name = name
|
self.process_name = name
|
||||||
|
|
||||||
@@ -37,7 +38,9 @@ class ServiceMessageFactory:
|
|||||||
)
|
)
|
||||||
return "null"
|
return "null"
|
||||||
|
|
||||||
def of(self, message_type: ServiceMessageType, payload: str, recipient_name: str) -> ServiceMessage:
|
def of(
|
||||||
|
self, message_type: ServiceMessageType, payload: str, recipient_name: str
|
||||||
|
) -> ServiceMessage:
|
||||||
"""
|
"""
|
||||||
Builds ServiceMessage object from relevant values.
|
Builds ServiceMessage object from relevant values.
|
||||||
:param message_type: enum Message type
|
:param message_type: enum Message type
|
||||||
@@ -53,10 +56,12 @@ class ServiceMessageFactory:
|
|||||||
recipient_name=recipient_name,
|
recipient_name=recipient_name,
|
||||||
reply_to=self.process_name,
|
reply_to=self.process_name,
|
||||||
trace_info=TraceInfoAdapter.create_produce_span(_id),
|
trace_info=TraceInfoAdapter.create_produce_span(_id),
|
||||||
message=payload
|
message=payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
def as_base64(self, message_type: ServiceMessageType, payload: str, recipient_name: str) -> ServiceMessage:
|
def as_base64(
|
||||||
|
self, message_type: ServiceMessageType, payload: str, recipient_name: str
|
||||||
|
) -> ServiceMessage:
|
||||||
"""
|
"""
|
||||||
Builds ServiceMessage object from relevant values, stores content Base64 encoded.
|
Builds ServiceMessage object from relevant values, stores content Base64 encoded.
|
||||||
:param message_type: enum Message type
|
:param message_type: enum Message type
|
||||||
@@ -72,7 +77,7 @@ class ServiceMessageFactory:
|
|||||||
recipient_name=recipient_name,
|
recipient_name=recipient_name,
|
||||||
reply_to=self.process_name,
|
reply_to=self.process_name,
|
||||||
trace_info=TraceInfoAdapter.create_produce_span(_id.as_string()),
|
trace_info=TraceInfoAdapter.create_produce_span(_id.as_string()),
|
||||||
message=payload
|
message=payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
def from_bytes(self, input_bytes: bytes) -> ServiceMessage:
|
def from_bytes(self, input_bytes: bytes) -> ServiceMessage:
|
||||||
@@ -94,7 +99,9 @@ class ServiceMessageFactory:
|
|||||||
i += 1
|
i += 1
|
||||||
reply_to = input_str_array[i] if i < len(input_str_array) else ""
|
reply_to = input_str_array[i] if i < len(input_str_array) else ""
|
||||||
i += 1
|
i += 1
|
||||||
trace_info = parse_map_string(input_str_array[i]) if i < len(input_str_array) else {}
|
trace_info = (
|
||||||
|
parse_map_string(input_str_array[i]) if i < len(input_str_array) else {}
|
||||||
|
)
|
||||||
i += 1
|
i += 1
|
||||||
message = input_str_array[i] if i < len(input_str_array) else ""
|
message = input_str_array[i] if i < len(input_str_array) else ""
|
||||||
return ServiceMessage(
|
return ServiceMessage(
|
||||||
@@ -104,14 +111,20 @@ class ServiceMessageFactory:
|
|||||||
recipient_name=recipient_name,
|
recipient_name=recipient_name,
|
||||||
reply_to=reply_to,
|
reply_to=reply_to,
|
||||||
trace_info=trace_info,
|
trace_info=trace_info,
|
||||||
message=base64.b64decode(message).decode() if payload_type & 0x80 else message
|
message=(
|
||||||
|
base64.b64decode(message).decode()
|
||||||
|
if payload_type & 0x80
|
||||||
|
else message
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(
|
logging_error(
|
||||||
"Unexpected ServiceMessage format. Expected 6 tokens separated by '|' in format: "
|
"Unexpected ServiceMessage format. Expected 6 tokens separated by '|' in format: "
|
||||||
"[ID(64bit number)|MessageType(8bit number)|RecipientName(String)|"
|
"[ID(64bit number)|MessageType(8bit number)|RecipientName(String)|"
|
||||||
"ReplyTo(String)|TraceInfo|Payload(String or Base64)], got: "
|
"ReplyTo(String)|TraceInfo|Payload(String or Base64)], got: "
|
||||||
"[%s], reported problem: %s", input_bytes.decode(), str(e)
|
"[%s], reported problem: %s",
|
||||||
|
input_bytes.decode(),
|
||||||
|
str(e),
|
||||||
)
|
)
|
||||||
return INVALID_MESSAGE
|
return INVALID_MESSAGE
|
||||||
|
|
||||||
@@ -125,7 +138,7 @@ class ServiceMessageFactory:
|
|||||||
f"{message.id.as_string()}{RS}{self.format_message_type(message)}{RS}{message.recipient_name}{RS}"
|
f"{message.id.as_string()}{RS}{self.format_message_type(message)}{RS}{message.recipient_name}{RS}"
|
||||||
f"{message.reply_to}{RS}{map_as_string(message.trace_info)}{RS}"
|
f"{message.reply_to}{RS}{map_as_string(message.trace_info)}{RS}"
|
||||||
f"{base64.b64encode(message.message.encode()).decode() if message.payload_type != 0 else message.message}"
|
f"{base64.b64encode(message.message.encode()).decode() if message.payload_type != 0 else message.message}"
|
||||||
).encode('utf-8')
|
).encode("utf-8")
|
||||||
|
|
||||||
def format_message_type(self, message: ServiceMessage) -> str:
|
def format_message_type(self, message: ServiceMessage) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -134,6 +147,10 @@ class ServiceMessageFactory:
|
|||||||
:param message: Service message to send
|
:param message: Service message to send
|
||||||
:return: formatted message type
|
:return: formatted message type
|
||||||
"""
|
"""
|
||||||
num_type = (message.message_type.value if message.message_type else ServiceMessageType.UNKNOWN.value) - 1
|
num_type = (
|
||||||
fmt = ((num_type & 0x7F) | (message.payload_type & 0x80))
|
message.message_type.value
|
||||||
|
if message.message_type
|
||||||
|
else ServiceMessageType.UNKNOWN.value
|
||||||
|
) - 1
|
||||||
|
fmt = (num_type & 0x7F) | (message.payload_type & 0x80)
|
||||||
return f"{fmt:02X}"
|
return f"{fmt:02X}"
|
||||||
|
|||||||
@@ -7,14 +7,24 @@ from amqp.model.model import AMQRoute
|
|||||||
class TestAMQRouteFactory(TestCase):
|
class TestAMQRouteFactory(TestCase):
|
||||||
def test_from_string(self):
|
def test_from_string(self):
|
||||||
_f = AMQRouteFactory()
|
_f = AMQRouteFactory()
|
||||||
_r: AMQRoute = _f.from_string("amq-clever-pybook::amq-clever-pybook:clever.pybook.localhost.#:5:0")
|
_r: AMQRoute = _f.from_string(
|
||||||
|
"amq-clever-pybook::amq-clever-pybook:clever.pybook.localhost.#:5:0"
|
||||||
|
)
|
||||||
self.assertEqual("", _r.exchange)
|
self.assertEqual("", _r.exchange)
|
||||||
self.assertEqual("amq-clever-pybook", _r.queue)
|
self.assertEqual("amq-clever-pybook", _r.queue)
|
||||||
self.assertEqual(5, _r.timeout)
|
self.assertEqual(5, _r.timeout)
|
||||||
_r2: AMQRoute = _f.from_string("amq-clever-pybook:amq-clever-pybook:clever.pybook.localhost.#:5")
|
_r2: AMQRoute = _f.from_string(
|
||||||
|
"amq-clever-pybook:amq-clever-pybook:clever.pybook.localhost.#:5"
|
||||||
|
)
|
||||||
self.assertEqual(NULL_ROUTE, _r2)
|
self.assertEqual(NULL_ROUTE, _r2)
|
||||||
|
|
||||||
def test_validate(self):
|
def test_validate(self):
|
||||||
_f = AMQRouteFactory()
|
_f = AMQRouteFactory()
|
||||||
self.assertTrue(_f.validate("amq-clever-pybook::amq-clever-pybook:clever.pybook.localhost.#:5:0"))
|
self.assertTrue(
|
||||||
self.assertFalse(_f.validate("amq-clever-pybook:amq-clever-pybook:clever.pybook.localhost.#"))
|
_f.validate(
|
||||||
|
"amq-clever-pybook::amq-clever-pybook:clever.pybook.localhost.#:5:0"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
_f.validate("amq-clever-pybook:amq-clever-pybook:clever.pybook.localhost.#")
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,37 +9,76 @@ class DataMessageFactoryTest(TestCase):
|
|||||||
raw = "\002\027A64f3b5af4d00000a4000|m=POST|d=clever-pybook.localhost|p=/debug|h={Accept=[*/*],X-Forwarded-Host=[clever-pybook.localhost:8085],User-Agent=[curl/8.7.1],X-Forwarded-Proto=[http],X-Forwarded-For=[172.18.0.1],Host=[clever-pybook.localhost:8085],Accept-Encoding=[gzip],Content-Length=[1475],X-Forwarded-Port=[8085],X-Real-Ip=[172.18.0.1],X-Forwarded-Server=[c5945bef67d1],Content-Type=[multipart/form-data; boundary=------------------------j1RjIqLrFH07XMsyMvpS1v]}|t={traceparent=00-bed6eb7de0d8ccf4c80b145a4bfec357-8d1f3bafc471bf0f-01}|b=TEST_ME"
|
raw = "\002\027A64f3b5af4d00000a4000|m=POST|d=clever-pybook.localhost|p=/debug|h={Accept=[*/*],X-Forwarded-Host=[clever-pybook.localhost:8085],User-Agent=[curl/8.7.1],X-Forwarded-Proto=[http],X-Forwarded-For=[172.18.0.1],Host=[clever-pybook.localhost:8085],Accept-Encoding=[gzip],Content-Length=[1475],X-Forwarded-Port=[8085],X-Real-Ip=[172.18.0.1],X-Forwarded-Server=[c5945bef67d1],Content-Type=[multipart/form-data; boundary=------------------------j1RjIqLrFH07XMsyMvpS1v]}|t={traceparent=00-bed6eb7de0d8ccf4c80b145a4bfec357-8d1f3bafc471bf0f-01}|b=TEST_ME"
|
||||||
msg = DataMessageFactory.from_bytes(raw.encode("utf-8"))
|
msg = DataMessageFactory.from_bytes(raw.encode("utf-8"))
|
||||||
self.assertIsNotNone(msg.id)
|
self.assertIsNotNone(msg.id)
|
||||||
self.assertEqual(msg.id.as_string(),'64F3B5AF4D00000A4000'.lower(),'SnowflakeID incorrectly parsed.')
|
self.assertEqual(
|
||||||
self.assertEqual(msg.headers["Content-Type"][0], "multipart/form-data; boundary=------------------------j1RjIqLrFH07XMsyMvpS1v")
|
msg.id.as_string(),
|
||||||
|
"64F3B5AF4D00000A4000".lower(),
|
||||||
|
"SnowflakeID incorrectly parsed.",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
msg.headers["Content-Type"][0],
|
||||||
|
"multipart/form-data; boundary=------------------------j1RjIqLrFH07XMsyMvpS1v",
|
||||||
|
)
|
||||||
|
|
||||||
def test_generate_snowflake_id(self):
|
def test_generate_snowflake_id(self):
|
||||||
_id = DataMessageFactory.get_instance(1).generate_snowflake_id()
|
_id = DataMessageFactory.get_instance(1).generate_snowflake_id()
|
||||||
self.assertEqual('00001000', _id.as_string()[-8:], 'Instance ID not set correctly')
|
self.assertEqual(
|
||||||
|
"00001000", _id.as_string()[-8:], "Instance ID not set correctly"
|
||||||
|
)
|
||||||
|
|
||||||
_id = DataMessageFactory(2).generate_snowflake_id()
|
_id = DataMessageFactory(2).generate_snowflake_id()
|
||||||
self.assertEqual('00002000', _id.as_string()[-8:], 'Instance ID not set correctly')
|
self.assertEqual(
|
||||||
|
"00002000", _id.as_string()[-8:], "Instance ID not set correctly"
|
||||||
|
)
|
||||||
|
|
||||||
_f = DataMessageFactory(127)
|
_f = DataMessageFactory(127)
|
||||||
_i0 = _f.generate_snowflake_id()
|
_i0 = _f.generate_snowflake_id()
|
||||||
_i1 = _f.generate_snowflake_id()
|
_i1 = _f.generate_snowflake_id()
|
||||||
_i2 = _f.generate_snowflake_id()
|
_i2 = _f.generate_snowflake_id()
|
||||||
_i3 = _f.generate_snowflake_id()
|
_i3 = _f.generate_snowflake_id()
|
||||||
self.assertEqual('0007F000'.lower(), _i0.as_string()[-8:], 'Instance ID and sequence not set correctly')
|
self.assertEqual(
|
||||||
self.assertEqual('0007F001'.lower(), _i1.as_string()[-8:], 'Instance ID and sequence not set correctly')
|
"0007F000".lower(),
|
||||||
self.assertEqual('0007F002'.lower(), _i2.as_string()[-8:], 'Instance ID and sequence not set correctly')
|
_i0.as_string()[-8:],
|
||||||
self.assertEqual('0007F003'.lower(), _i3.as_string()[-8:], 'Instance ID and sequence not set correctly')
|
"Instance ID and sequence not set correctly",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"0007F001".lower(),
|
||||||
|
_i1.as_string()[-8:],
|
||||||
|
"Instance ID and sequence not set correctly",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"0007F002".lower(),
|
||||||
|
_i2.as_string()[-8:],
|
||||||
|
"Instance ID and sequence not set correctly",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"0007F003".lower(),
|
||||||
|
_i3.as_string()[-8:],
|
||||||
|
"Instance ID and sequence not set correctly",
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_request_message(self):
|
def test_create_request_message(self):
|
||||||
_f = DataMessageFactory(34)
|
_f = DataMessageFactory(34)
|
||||||
_req = _f.create_request_message("PUT", "localhost", "/test/me", {"k1": ["v1"], "k2": ["v2"]}, "Quick brown fox")
|
_req = _f.create_request_message(
|
||||||
self.assertEqual("localhost", _req.domain, 'Domain not set correctly')
|
"PUT",
|
||||||
self.assertEqual("/test/me", _req.path, 'Context path not set correctly')
|
"localhost",
|
||||||
self.assertEqual("v1", _req.headers['k1'][0], "Headers not set correctly")
|
"/test/me",
|
||||||
self.assertEqual(b'Quick brown fox', _req.body(), "Body not correctly decoded")
|
{"k1": ["v1"], "k2": ["v2"]},
|
||||||
|
"Quick brown fox",
|
||||||
|
)
|
||||||
|
self.assertEqual("localhost", _req.domain, "Domain not set correctly")
|
||||||
|
self.assertEqual("/test/me", _req.path, "Context path not set correctly")
|
||||||
|
self.assertEqual("v1", _req.headers["k1"][0], "Headers not set correctly")
|
||||||
|
self.assertEqual(b"Quick brown fox", _req.body(), "Body not correctly decoded")
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
_f = DataMessageFactory(34)
|
_f = DataMessageFactory(34)
|
||||||
_req = _f.create_request_message("PUT", "localhost", "/test/me", {"k1": ["v1"], "k2": ["v2"]}, "Quick brown fox")
|
_req = _f.create_request_message(
|
||||||
|
"PUT",
|
||||||
|
"localhost",
|
||||||
|
"/test/me",
|
||||||
|
{"k1": ["v1"], "k2": ["v2"]},
|
||||||
|
"Quick brown fox",
|
||||||
|
)
|
||||||
_ser = _f.serialize(_req)
|
_ser = _f.serialize(_req)
|
||||||
self.assertEqual(65, _ser[2])
|
self.assertEqual(65, _ser[2])
|
||||||
_deser = _f.from_bytes(_ser)
|
_deser = _f.from_bytes(_ser)
|
||||||
@@ -51,18 +90,40 @@ class DataMessageFactoryTest(TestCase):
|
|||||||
|
|
||||||
def test_amq_message_routing_key(self):
|
def test_amq_message_routing_key(self):
|
||||||
_f = DataMessageFactory(35)
|
_f = DataMessageFactory(35)
|
||||||
_req = _f.create_request_message("PUT", "localhost", "/test/me", {"k1": ["v1"], "k2": ["v2"]}, "Quick brown fox")
|
_req = _f.create_request_message(
|
||||||
|
"PUT",
|
||||||
|
"localhost",
|
||||||
|
"/test/me",
|
||||||
|
{"k1": ["v1"], "k2": ["v2"]},
|
||||||
|
"Quick brown fox",
|
||||||
|
)
|
||||||
_key = _f.amq_message_routing_key(_req)
|
_key = _f.amq_message_routing_key(_req)
|
||||||
self.assertEqual("localhost.test.me", _key, "Message routing key wrongly extracted from the message")
|
self.assertEqual(
|
||||||
|
"localhost.test.me",
|
||||||
|
_key,
|
||||||
|
"Message routing key wrongly extracted from the message",
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_timestamp_from_id(self):
|
def test_get_timestamp_from_id(self):
|
||||||
_f = DataMessageFactory(35)
|
_f = DataMessageFactory(35)
|
||||||
_timestamp = _f.get_timestamp_from_id(_f.generate_snowflake_id())
|
_timestamp = _f.get_timestamp_from_id(_f.generate_snowflake_id())
|
||||||
_x_timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)
|
_x_timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||||
self.assertLessEqual(_x_timestamp - _timestamp, 1, "Timestamp in SnowflakeId differs too much from current time")
|
self.assertLessEqual(
|
||||||
|
_x_timestamp - _timestamp,
|
||||||
|
1,
|
||||||
|
"Timestamp in SnowflakeId differs too much from current time",
|
||||||
|
)
|
||||||
|
|
||||||
def test_to_string(self):
|
def test_to_string(self):
|
||||||
_f = DataMessageFactory(35)
|
_f = DataMessageFactory(35)
|
||||||
_req = _f.create_request_message("PUT", "localhost", "/test/me", {"k1": ["v1"], "k2": ["v2"]}, "Quick brown fox")
|
_req = _f.create_request_message(
|
||||||
|
"PUT",
|
||||||
|
"localhost",
|
||||||
|
"/test/me",
|
||||||
|
{"k1": ["v1"], "k2": ["v2"]},
|
||||||
|
"Quick brown fox",
|
||||||
|
)
|
||||||
_as_str = _f.to_string(_req)
|
_as_str = _f.to_string(_req)
|
||||||
self.assertGreater(_as_str.index("domain='localhost'"), 0, "Expected content not found")
|
self.assertGreater(
|
||||||
|
_as_str.index("domain='localhost'"), 0, "Expected content not found"
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ class TestDataResponseFactory(TestCase):
|
|||||||
_snowflakeId: SnowflakeId = _g.generate_snowflake_id()
|
_snowflakeId: SnowflakeId = _g.generate_snowflake_id()
|
||||||
_resp = _f.create_async_response_message(_snowflakeId)
|
_resp = _f.create_async_response_message(_snowflakeId)
|
||||||
_ser = _f.serialize(_resp)
|
_ser = _f.serialize(_resp)
|
||||||
self.assertEqual(2, str(_ser[2:]).index(_f.MAGIC_BYTE_HTTP_REQUEST+_snowflakeId.as_string()), "Magic byte and SnowflakeId not found")
|
self.assertEqual(
|
||||||
|
2,
|
||||||
|
str(_ser[2:]).index(_f.MAGIC_BYTE_HTTP_REQUEST + _snowflakeId.as_string()),
|
||||||
|
"Magic byte and SnowflakeId not found",
|
||||||
|
)
|
||||||
self.assertGreater(str(_ser[2:]).index("|e=|"), 2)
|
self.assertGreater(str(_ser[2:]).index("|e=|"), 2)
|
||||||
self.assertEqual((_ser[0]+_ser[1]), len(_ser) - 2 - len('T0s='))
|
self.assertEqual((_ser[0] + _ser[1]), len(_ser) - 2 - len("T0s="))
|
||||||
|
|
||||||
def test_from_bytes(self):
|
def test_from_bytes(self):
|
||||||
_f = DataResponseFactory()
|
_f = DataResponseFactory()
|
||||||
@@ -33,4 +37,4 @@ class TestDataResponseFactory(TestCase):
|
|||||||
_deser = _f.from_bytes(_ser)
|
_deser = _f.from_bytes(_ser)
|
||||||
self.assertEqual(_snowflakeId.as_string(), _deser.id)
|
self.assertEqual(_snowflakeId.as_string(), _deser.id)
|
||||||
self.assertEqual(200, _deser.response_code)
|
self.assertEqual(200, _deser.response_code)
|
||||||
self.assertEqual(b'OK', _deser.response)
|
self.assertEqual(b"OK", _deser.response)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from amqp.adapter.file_downloader import ensure_directory_exists
|
from amqp.adapter.file_downloader import ensure_directory_exists
|
||||||
|
|
||||||
|
|
||||||
class TestFileDownloader(TestCase):
|
class TestFileDownloader(TestCase):
|
||||||
def test_ensure_directory_exists(self):
|
def test_ensure_directory_exists(self):
|
||||||
"""
|
"""
|
||||||
@@ -30,7 +31,9 @@ class TestFileDownloader(TestCase):
|
|||||||
with open(filepath2, "w") as f:
|
with open(filepath2, "w") as f:
|
||||||
f.write("test")
|
f.write("test")
|
||||||
result2_v1 = ensure_directory_exists(filepath2)
|
result2_v1 = ensure_directory_exists(filepath2)
|
||||||
assert result2_v1 == os.path.join(temp_dir, "test2.1.txt"), f"Test Case 2 Version 1 Failed: {result2_v1} != {os.path.join(temp_dir, 'test2.1.txt')}"
|
assert result2_v1 == os.path.join(
|
||||||
|
temp_dir, "test2.1.txt"
|
||||||
|
), f"Test Case 2 Version 1 Failed: {result2_v1} != {os.path.join(temp_dir, 'test2.1.txt')}"
|
||||||
|
|
||||||
# Create a few versioned files
|
# Create a few versioned files
|
||||||
with open(os.path.join(temp_dir, "test3.txt"), "w") as f:
|
with open(os.path.join(temp_dir, "test3.txt"), "w") as f:
|
||||||
@@ -41,6 +44,8 @@ class TestFileDownloader(TestCase):
|
|||||||
f.write("test")
|
f.write("test")
|
||||||
filepath3 = os.path.join(temp_dir, "test3.txt")
|
filepath3 = os.path.join(temp_dir, "test3.txt")
|
||||||
result3_v3 = ensure_directory_exists(filepath3)
|
result3_v3 = ensure_directory_exists(filepath3)
|
||||||
assert result3_v3 == os.path.join(temp_dir, "test3.3.txt"), f"Test Case 3 Version 3 Failed: {result3_v3} != {os.path.join(temp_dir, 'test3.3.txt')}"
|
assert result3_v3 == os.path.join(
|
||||||
|
temp_dir, "test3.3.txt"
|
||||||
|
), f"Test Case 3 Version 3 Failed: {result3_v3} != {os.path.join(temp_dir, 'test3.3.txt')}"
|
||||||
|
|
||||||
print("All test cases passed!")
|
print("All test cases passed!")
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ class PydanticSerializerTest(TestCase):
|
|||||||
assert False
|
assert False
|
||||||
|
|
||||||
def test_parse_url_query(self):
|
def test_parse_url_query(self):
|
||||||
path, args = parse_url_query("http://localhost:8080/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741?response_type=JsonFile")
|
path, args = parse_url_query(
|
||||||
|
"http://localhost:8080/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741?response_type=JsonFile"
|
||||||
|
)
|
||||||
assert path == "/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741"
|
assert path == "/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741"
|
||||||
assert args == {'response_type': 'JsonFile'}
|
assert args == {"response_type": "JsonFile"}
|
||||||
path, args = parse_url_query("http://localhost:8080/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741")
|
path, args = parse_url_query(
|
||||||
|
"http://localhost:8080/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741"
|
||||||
|
)
|
||||||
assert path == "/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741"
|
assert path == "/api/v0/unstructured/863e4d9f-4612-4173-8d0a-3de34cc7a741"
|
||||||
assert args == {}
|
assert args == {}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from amqp.adapter.service_message_factory import ServiceMessageFactory, RS, INVALID_MESSAGE
|
from amqp.adapter.service_message_factory import (
|
||||||
|
ServiceMessageFactory,
|
||||||
|
RS,
|
||||||
|
INVALID_MESSAGE,
|
||||||
|
)
|
||||||
from amqp.model.model import ServiceMessage
|
from amqp.model.model import ServiceMessage
|
||||||
from amqp.model.service_message_type import ServiceMessageType
|
from amqp.model.service_message_type import ServiceMessageType
|
||||||
|
|
||||||
@@ -8,17 +12,21 @@ from amqp.model.service_message_type import ServiceMessageType
|
|||||||
class ServiceMessageTest(TestCase):
|
class ServiceMessageTest(TestCase):
|
||||||
|
|
||||||
def test_to_string(self):
|
def test_to_string(self):
|
||||||
_f: ServiceMessageFactory = ServiceMessageFactory('bumble-bee')
|
_f: ServiceMessageFactory = ServiceMessageFactory("bumble-bee")
|
||||||
message: ServiceMessage = _f.of(ServiceMessageType.ROUTING_DATA, "Duck", "Donald")
|
message: ServiceMessage = _f.of(
|
||||||
|
ServiceMessageType.ROUTING_DATA, "Duck", "Donald"
|
||||||
|
)
|
||||||
message_str = _f.to_string(message)
|
message_str = _f.to_string(message)
|
||||||
self.assertTrue("|type=01|" in message_str)
|
self.assertTrue("|type=01|" in message_str)
|
||||||
self.assertTrue("|body=Duck" in message_str)
|
self.assertTrue("|body=Duck" in message_str)
|
||||||
message: ServiceMessage = _f.of(ServiceMessageType.ROUTING_DATA_REPLACE, "Duck", "Donald")
|
message: ServiceMessage = _f.of(
|
||||||
|
ServiceMessageType.ROUTING_DATA_REPLACE, "Duck", "Donald"
|
||||||
|
)
|
||||||
message_str = _f.to_string(message)
|
message_str = _f.to_string(message)
|
||||||
self.assertTrue("|type=03|" in message_str)
|
self.assertTrue("|type=03|" in message_str)
|
||||||
self.assertTrue("|body=Duck" in message_str)
|
self.assertTrue("|body=Duck" in message_str)
|
||||||
self.assertTrue("|replyTo=bumble-bee" in message_str)
|
self.assertTrue("|replyTo=bumble-bee" in message_str)
|
||||||
nullMsg = _f.to_string(None)
|
nullMsg = _f.to_string(None)
|
||||||
self.assertEqual("null", nullMsg)
|
self.assertEqual("null", nullMsg)
|
||||||
|
|
||||||
def test_from(self):
|
def test_from(self):
|
||||||
@@ -31,7 +39,7 @@ class ServiceMessageTest(TestCase):
|
|||||||
assert deserialized.recipient_name == "Donald"
|
assert deserialized.recipient_name == "Donald"
|
||||||
assert deserialized.message == "Duck"
|
assert deserialized.message == "Duck"
|
||||||
assert deserialized.reply_to == "amqp-router"
|
assert deserialized.reply_to == "amqp-router"
|
||||||
invalidMsg = _f.from_bytes(b'wrong')
|
invalidMsg = _f.from_bytes(b"wrong")
|
||||||
self.assertEqual(invalidMsg, INVALID_MESSAGE)
|
self.assertEqual(invalidMsg, INVALID_MESSAGE)
|
||||||
|
|
||||||
def test_base64(self):
|
def test_base64(self):
|
||||||
@@ -43,7 +51,7 @@ class ServiceMessageTest(TestCase):
|
|||||||
assert deserialized.is_valid()
|
assert deserialized.is_valid()
|
||||||
assert deserialized.message_type == ServiceMessageType.ROUTING_DATA
|
assert deserialized.message_type == ServiceMessageType.ROUTING_DATA
|
||||||
assert deserialized.message == "Duck"
|
assert deserialized.message == "Duck"
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
_f: ServiceMessageFactory = ServiceMessageFactory("amqp-router")
|
_f: ServiceMessageFactory = ServiceMessageFactory("amqp-router")
|
||||||
message = _f.of(ServiceMessageType.ROUTING_DATA, "Duck", "Donald")
|
message = _f.of(ServiceMessageType.ROUTING_DATA, "Duck", "Donald")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TraceInfoAdapter:
|
class TraceInfoAdapter:
|
||||||
"""
|
"""
|
||||||
@@ -9,6 +10,4 @@ class TraceInfoAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_produce_span(message_id: str) -> Dict[str, str]:
|
def create_produce_span(message_id: str) -> Dict[str, str]:
|
||||||
return {
|
return {"trace-id": message_id}
|
||||||
"trace-id": message_id
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class AMQAdapter:
|
|||||||
configuration with prefix 'cm.amq-adapter'
|
configuration with prefix 'cm.amq-adapter'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX: str = 'cm.amq-adapter.'
|
PREFIX: str = "cm.amq-adapter."
|
||||||
|
|
||||||
def __init__(self, config: configparser.ConfigParser):
|
def __init__(self, config: configparser.ConfigParser):
|
||||||
"""
|
"""
|
||||||
@@ -24,12 +24,21 @@ class AMQAdapter:
|
|||||||
|
|
||||||
:param config: config parser to read configuration values
|
:param config: config parser to read configuration values
|
||||||
"""
|
"""
|
||||||
self.generator_id = config.getint('CleverMicro-AMQ', self.PREFIX + 'generator-id', fallback=164)
|
self.generator_id = config.getint(
|
||||||
self.service_name = config.get('CleverMicro-AMQ', self.PREFIX + 'service-name',
|
"CleverMicro-AMQ", self.PREFIX + "generator-id", fallback=164
|
||||||
fallback='amq-python-adapter-' + str(self.generator_id))
|
)
|
||||||
self.route_mapping = config.get('CleverMicro-AMQ', self.PREFIX + 'route-mapping', fallback=None)
|
self.service_name = config.get(
|
||||||
|
"CleverMicro-AMQ",
|
||||||
|
self.PREFIX + "service-name",
|
||||||
|
fallback="amq-python-adapter-" + str(self.generator_id),
|
||||||
|
)
|
||||||
|
self.route_mapping = config.get(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "route-mapping", fallback=None
|
||||||
|
)
|
||||||
self.require_authenticated_user = config.get(
|
self.require_authenticated_user = config.get(
|
||||||
'CleverMicro-AMQ', self.PREFIX + 'require-authenticated-user', fallback=False
|
"CleverMicro-AMQ",
|
||||||
|
self.PREFIX + "require-authenticated-user",
|
||||||
|
fallback=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def adapter_prefix(self) -> str:
|
def adapter_prefix(self) -> str:
|
||||||
@@ -41,7 +50,7 @@ class Dispatch:
|
|||||||
configuration with prefix 'cm.dispatch'
|
configuration with prefix 'cm.dispatch'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX: str = 'cm.dispatch.'
|
PREFIX: str = "cm.dispatch."
|
||||||
|
|
||||||
def __init__(self, config: configparser.ConfigParser):
|
def __init__(self, config: configparser.ConfigParser):
|
||||||
"""
|
"""
|
||||||
@@ -55,24 +64,43 @@ class Dispatch:
|
|||||||
|
|
||||||
:param config: config parser to read configuration values
|
:param config: config parser to read configuration values
|
||||||
"""
|
"""
|
||||||
self.use_dlq = config.getboolean('CleverMicro-AMQ', self.PREFIX + 'use-dlq', fallback=False)
|
self.use_dlq = config.getboolean(
|
||||||
self.use_confirms = config.getboolean('CleverMicro-AMQ', self.PREFIX + 'use-confirms', fallback=False)
|
"CleverMicro-AMQ", self.PREFIX + "use-dlq", fallback=False
|
||||||
self.amq_host = config.get('CleverMicro-AMQ', self.PREFIX + 'amq-host', fallback='rabbitmq')
|
)
|
||||||
self.amq_port = config.getint('CleverMicro-AMQ', self.PREFIX + 'amq-port', fallback=5672)
|
self.use_confirms = config.getboolean(
|
||||||
self.amq_port_tls = config.getint('CleverMicro-AMQ', self.PREFIX + 'amq-port-tls', fallback=5671)
|
"CleverMicro-AMQ", self.PREFIX + "use-confirms", fallback=False
|
||||||
self.service_host = config.get('CleverMicro-AMQ', self.PREFIX + 'service-host', fallback='localhost')
|
)
|
||||||
self.service_port = config.getint('CleverMicro-AMQ', self.PREFIX + 'service-port', fallback=8080)
|
self.amq_host = config.get(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "amq-host", fallback="rabbitmq"
|
||||||
|
)
|
||||||
|
self.amq_port = config.getint(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "amq-port", fallback=5672
|
||||||
|
)
|
||||||
|
self.amq_port_tls = config.getint(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "amq-port-tls", fallback=5671
|
||||||
|
)
|
||||||
|
self.service_host = config.get(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "service-host", fallback="localhost"
|
||||||
|
)
|
||||||
|
self.service_port = config.getint(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "service-port", fallback=8080
|
||||||
|
)
|
||||||
self.rabbit_mq_user = os.getenv("RABBIT_MQ_USER", "springuser")
|
self.rabbit_mq_user = os.getenv("RABBIT_MQ_USER", "springuser")
|
||||||
self.rabbit_mq_password = os.getenv("RABBIT_MQ_PASSWORD", "TheCleverWho")
|
self.rabbit_mq_password = os.getenv("RABBIT_MQ_PASSWORD", "TheCleverWho")
|
||||||
self.rabbit_mq_url = f"amqp://{self.rabbit_mq_user}:{self.rabbit_mq_password}@{self.amq_host}/"
|
self.rabbit_mq_url = (
|
||||||
self.download_dir = config.get('CleverMicro-AMQ', self.PREFIX + 'download-dir', fallback='downloaded_files')
|
f"amqp://{self.rabbit_mq_user}:{self.rabbit_mq_password}@{self.amq_host}/"
|
||||||
|
)
|
||||||
|
self.download_dir = config.get(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "download-dir", fallback="downloaded_files"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Backpressure:
|
class Backpressure:
|
||||||
"""
|
"""
|
||||||
configuration with prefix 'cm.backpressure'
|
configuration with prefix 'cm.backpressure'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX: str = 'cm.backpressure.'
|
PREFIX: str = "cm.backpressure."
|
||||||
|
|
||||||
def __init__(self, config: configparser.ConfigParser):
|
def __init__(self, config: configparser.ConfigParser):
|
||||||
"""
|
"""
|
||||||
@@ -81,15 +109,20 @@ class Backpressure:
|
|||||||
|
|
||||||
:param config: config parser to read configuration values
|
:param config: config parser to read configuration values
|
||||||
"""
|
"""
|
||||||
self.threshold = config.getint('CleverMicro-AMQ', self.PREFIX + 'threshold', fallback=0)
|
self.threshold = config.getint(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "threshold", fallback=0
|
||||||
|
)
|
||||||
# time-window is in milliseconds, to report backpressure IDLE alert
|
# time-window is in milliseconds, to report backpressure IDLE alert
|
||||||
self.time_window = config.getint('CleverMicro-AMQ', self.PREFIX + 'time-window', fallback=10000)
|
self.time_window = config.getint(
|
||||||
|
"CleverMicro-AMQ", self.PREFIX + "time-window", fallback=10000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AMQConfiguration:
|
class AMQConfiguration:
|
||||||
"""
|
"""
|
||||||
Top level configuration context holder / object
|
Top level configuration context holder / object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config_file_name: str = None):
|
def __init__(self, config_file_name: str = None):
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
# Read the application.properties file
|
# Read the application.properties file
|
||||||
@@ -97,7 +130,9 @@ class AMQConfiguration:
|
|||||||
config.read(config_file_name)
|
config.read(config_file_name)
|
||||||
else:
|
else:
|
||||||
logging_error(f" [E] Configuration file not found: {config_file_name}")
|
logging_error(f" [E] Configuration file not found: {config_file_name}")
|
||||||
logging.warning(" [W] Defaulting to preset values, which will likely be wrong, causing errors.")
|
logging.warning(
|
||||||
|
" [W] Defaulting to preset values, which will likely be wrong, causing errors."
|
||||||
|
)
|
||||||
|
|
||||||
# Override values with environment variables
|
# Override values with environment variables
|
||||||
for section in config.sections():
|
for section in config.sections():
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
url = 'http://localhost:8080/amq-adapter-healthcheck'
|
url = "http://localhost:8080/amq-adapter-healthcheck"
|
||||||
headers = {'Authorization': 'Bearer my_access_token'}
|
headers = {"Authorization": "Bearer my_access_token"}
|
||||||
params = {'limit': 10, 'offset': 20}
|
params = {"limit": 10, "offset": 20}
|
||||||
response = requests.get(url, headers=headers, params=params)
|
response = requests.get(url, headers=headers, params=params)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
+80
-26
@@ -19,8 +19,17 @@ Define the structure of messages used in Clever Micro
|
|||||||
class CleverMicroMessage:
|
class CleverMicroMessage:
|
||||||
DEFAULT_CONTENT_TYPE = ["application/octet-stream"]
|
DEFAULT_CONTENT_TYPE = ["application/octet-stream"]
|
||||||
|
|
||||||
def __init__(self, magic: str, id: SnowflakeId, m_field: str, d_field: str, p_field: str,
|
def __init__(
|
||||||
headers: Dict[str, List[str]], trace_info: Dict[str, str], base64body: bytes):
|
self,
|
||||||
|
magic: str,
|
||||||
|
id: SnowflakeId,
|
||||||
|
m_field: str,
|
||||||
|
d_field: str,
|
||||||
|
p_field: str,
|
||||||
|
headers: Dict[str, List[str]],
|
||||||
|
trace_info: Dict[str, str],
|
||||||
|
base64body: bytes,
|
||||||
|
):
|
||||||
self._magic = magic
|
self._magic = magic
|
||||||
self._id: SnowflakeId = id
|
self._id: SnowflakeId = id
|
||||||
self._m_field = m_field
|
self._m_field = m_field
|
||||||
@@ -62,9 +71,20 @@ class CleverMicroMessage:
|
|||||||
|
|
||||||
|
|
||||||
class DataMessage(CleverMicroMessage):
|
class DataMessage(CleverMicroMessage):
|
||||||
def __init__(self, magic: str, id: SnowflakeId, method: str, domain: str, path: str,
|
def __init__(
|
||||||
headers: Dict[str, List[str]], trace_info: Dict[str, str], base64body: bytes):
|
self,
|
||||||
super().__init__(magic, id, method, domain, path, headers, trace_info, base64body)
|
magic: str,
|
||||||
|
id: SnowflakeId,
|
||||||
|
method: str,
|
||||||
|
domain: str,
|
||||||
|
path: str,
|
||||||
|
headers: Dict[str, List[str]],
|
||||||
|
trace_info: Dict[str, str],
|
||||||
|
base64body: bytes,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
magic, id, method, domain, path, headers, trace_info, base64body
|
||||||
|
)
|
||||||
|
|
||||||
def method(self) -> str:
|
def method(self) -> str:
|
||||||
return self.m_field()
|
return self.m_field()
|
||||||
@@ -77,9 +97,27 @@ class DataMessage(CleverMicroMessage):
|
|||||||
|
|
||||||
|
|
||||||
class DataResponse(CleverMicroMessage):
|
class DataResponse(CleverMicroMessage):
|
||||||
def __init__(self, magic: str, id: SnowflakeId, response_code: str, error: str, error_cause: str,
|
def __init__(
|
||||||
headers: Dict[str, List[str]], trace_info: Dict[str, str], base64body: bytes):
|
self,
|
||||||
super().__init__(magic, id, response_code, error, error_cause, headers, trace_info, base64body)
|
magic: str,
|
||||||
|
id: SnowflakeId,
|
||||||
|
response_code: str,
|
||||||
|
error: str,
|
||||||
|
error_cause: str,
|
||||||
|
headers: Dict[str, List[str]],
|
||||||
|
trace_info: Dict[str, str],
|
||||||
|
base64body: bytes,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
magic,
|
||||||
|
id,
|
||||||
|
response_code,
|
||||||
|
error,
|
||||||
|
error_cause,
|
||||||
|
headers,
|
||||||
|
trace_info,
|
||||||
|
base64body,
|
||||||
|
)
|
||||||
|
|
||||||
def response_code(self) -> str:
|
def response_code(self) -> str:
|
||||||
return self.m_field()
|
return self.m_field()
|
||||||
@@ -91,7 +129,6 @@ class DataResponse(CleverMicroMessage):
|
|||||||
return self.p_field()
|
return self.p_field()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AMQRoute:
|
class AMQRoute:
|
||||||
"""
|
"""
|
||||||
@@ -105,6 +142,7 @@ class AMQRoute:
|
|||||||
:param timeout: Timeout in milliseconds, how long to wait for reply, or not waiting if less then 1
|
:param timeout: Timeout in milliseconds, how long to wait for reply, or not waiting if less then 1
|
||||||
:param valid_until: optional timestamp (millis from epoch) after which the route is ignored
|
:param valid_until: optional timestamp (millis from epoch) after which the route is ignored
|
||||||
"""
|
"""
|
||||||
|
|
||||||
component_name: str
|
component_name: str
|
||||||
exchange: str
|
exchange: str
|
||||||
queue: str
|
queue: str
|
||||||
@@ -122,9 +160,11 @@ class AMQRoute:
|
|||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, AMQRoute):
|
if isinstance(other, AMQRoute):
|
||||||
return (self.component_name == other.component_name
|
return (
|
||||||
and self.queue == other.queue
|
self.component_name == other.component_name
|
||||||
and self.key == other.key)
|
and self.queue == other.queue
|
||||||
|
and self.key == other.key
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -147,6 +187,7 @@ class DataMessage:
|
|||||||
The response to this call is in format of DataResponse class.
|
The response to this call is in format of DataResponse class.
|
||||||
Please see DataMessageFactory for code to instantiate, serialize and deserialize the class
|
Please see DataMessageFactory for code to instantiate, serialize and deserialize the class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
magic: str
|
magic: str
|
||||||
id: SnowflakeId
|
id: SnowflakeId
|
||||||
method: str
|
method: str
|
||||||
@@ -165,10 +206,16 @@ class DataMessage:
|
|||||||
|
|
||||||
class MultipartDataMessage(DataMessage):
|
class MultipartDataMessage(DataMessage):
|
||||||
def __init__(self, message: DataMessage, extra_data: dict):
|
def __init__(self, message: DataMessage, extra_data: dict):
|
||||||
super().__init__(message.magic, message.id,
|
super().__init__(
|
||||||
message.method, message.domain, message.path,
|
message.magic,
|
||||||
message.headers, message.trace_info,
|
message.id,
|
||||||
message.base64_body)
|
message.method,
|
||||||
|
message.domain,
|
||||||
|
message.path,
|
||||||
|
message.headers,
|
||||||
|
message.trace_info,
|
||||||
|
message.base64_body,
|
||||||
|
)
|
||||||
self.extra_data = extra_data
|
self.extra_data = extra_data
|
||||||
|
|
||||||
|
|
||||||
@@ -178,6 +225,7 @@ class DataResponse:
|
|||||||
The response message to RPC style call (note: the request is made with DataMessage)
|
The response message to RPC style call (note: the request is made with DataMessage)
|
||||||
Please see DataResponseFactory for code to instantiate, serialize and deserialize the class
|
Please see DataResponseFactory for code to instantiate, serialize and deserialize the class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
response_code: int
|
response_code: int
|
||||||
content_type: str
|
content_type: str
|
||||||
@@ -200,6 +248,7 @@ class ServiceMessage:
|
|||||||
The CleverMicro service message that is used to communicate the internal state of the AMQ channel.
|
The CleverMicro service message that is used to communicate the internal state of the AMQ channel.
|
||||||
In v0.2 it supports service discovery / route registration and Backpressure status notification.
|
In v0.2 it supports service discovery / route registration and Backpressure status notification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: SnowflakeId = None
|
id: SnowflakeId = None
|
||||||
payload_type: int = 0
|
payload_type: int = 0
|
||||||
message_type: ServiceMessageType = ServiceMessageType.UNKNOWN
|
message_type: ServiceMessageType = ServiceMessageType.UNKNOWN
|
||||||
@@ -229,17 +278,20 @@ class ScalingRequest:
|
|||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
"""Converts the object to a JSON string matching the Java structure"""
|
"""Converts the object to a JSON string matching the Java structure"""
|
||||||
return json.dumps({
|
return json.dumps(
|
||||||
"version": self.version,
|
{
|
||||||
"serviceId": self.service_id,
|
"version": self.version,
|
||||||
"taskId": self.task_id,
|
"serviceId": self.service_id,
|
||||||
"maxAvailability": self.max_availability,
|
"taskId": self.task_id,
|
||||||
"currentAvailability": self.current_availability,
|
"maxAvailability": self.max_availability,
|
||||||
"requestType": self.request_type.value # Using .value for Enum serialization
|
"currentAvailability": self.current_availability,
|
||||||
}, indent=2)
|
"requestType": self.request_type.value, # Using .value for Enum serialization
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_str: str) -> 'ScalingRequest':
|
def from_json(cls, json_str: str) -> "ScalingRequest":
|
||||||
"""Creates a ScalingRequest from a JSON string"""
|
"""Creates a ScalingRequest from a JSON string"""
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
return cls(
|
return cls(
|
||||||
@@ -247,5 +299,7 @@ class ScalingRequest:
|
|||||||
task_id=data["taskId"],
|
task_id=data["taskId"],
|
||||||
max_availability=data["maxAvailability"],
|
max_availability=data["maxAvailability"],
|
||||||
current_availability=data["currentAvailability"],
|
current_availability=data["currentAvailability"],
|
||||||
request_type=ScalingRequestAlert(data["requestType"]) # Convert string to Enum
|
request_type=ScalingRequestAlert(
|
||||||
|
data["requestType"]
|
||||||
|
), # Convert string to Enum
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ class SnowflakeId:
|
|||||||
"""
|
"""
|
||||||
see https://en.wikipedia.org/wiki/Snowflake_ID
|
see https://en.wikipedia.org/wiki/Snowflake_ID
|
||||||
"""
|
"""
|
||||||
SNOWFLAKE_ID_WIDTH = 10 # with in bytes (8 bits each), can be more than long int width (8 bytes)
|
|
||||||
|
|
||||||
def __init__(self, hi_bits_or_bytes = None, lo_bits = None):
|
SNOWFLAKE_ID_WIDTH = (
|
||||||
|
10 # with in bytes (8 bits each), can be more than long int width (8 bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hi_bits_or_bytes=None, lo_bits=None):
|
||||||
self.bits = [0, 0]
|
self.bits = [0, 0]
|
||||||
if lo_bits is None:
|
if lo_bits is None:
|
||||||
if hi_bits_or_bytes is not None:
|
if hi_bits_or_bytes is not None:
|
||||||
@@ -39,13 +42,17 @@ class SnowflakeId:
|
|||||||
for i in range(0, expected_length, 2):
|
for i in range(0, expected_length, 2):
|
||||||
if i == expected_length - (2 * 8):
|
if i == expected_length - (2 * 8):
|
||||||
n = 0
|
n = 0
|
||||||
bits[n] = (bits[n] << 8) | int(_input[i:i + 2], 16)
|
bits[n] = (bits[n] << 8) | int(_input[i : i + 2], 16)
|
||||||
|
|
||||||
return SnowflakeId(bits[1], bits[0])
|
return SnowflakeId(bits[1], bits[0])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
_hex = f"{self.bits[1]:016X}{self.bits[0]:016X}"
|
_hex = f"{self.bits[1]:016X}{self.bits[0]:016X}"
|
||||||
_val = _hex if len(_hex) <= 2 * SnowflakeId.SNOWFLAKE_ID_WIDTH else _hex[(-2 * SnowflakeId.SNOWFLAKE_ID_WIDTH):]
|
_val = (
|
||||||
|
_hex
|
||||||
|
if len(_hex) <= 2 * SnowflakeId.SNOWFLAKE_ID_WIDTH
|
||||||
|
else _hex[(-2 * SnowflakeId.SNOWFLAKE_ID_WIDTH) :]
|
||||||
|
)
|
||||||
return _val.lower()
|
return _val.lower()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class SnowflakeIdTest(TestCase):
|
|||||||
_id_2 = DataMessageFactory.get_instance(1).generate_snowflake_id()
|
_id_2 = DataMessageFactory.get_instance(1).generate_snowflake_id()
|
||||||
self.assertTrue(_id_1 < _id_2)
|
self.assertTrue(_id_1 < _id_2)
|
||||||
|
|
||||||
|
|
||||||
def test_from_hex(self):
|
def test_from_hex(self):
|
||||||
_factory = DataMessageFactory.get_instance(1)
|
_factory = DataMessageFactory.get_instance(1)
|
||||||
_id_1 = _factory.generate_snowflake_id()
|
_id_1 = _factory.generate_snowflake_id()
|
||||||
@@ -27,8 +26,10 @@ class SnowflakeIdTest(TestCase):
|
|||||||
self.assertTrue(_id_1 == _id_2)
|
self.assertTrue(_id_1 == _id_2)
|
||||||
with self.assertRaises(RuntimeError) as context:
|
with self.assertRaises(RuntimeError) as context:
|
||||||
SnowflakeId.from_hex("100")
|
SnowflakeId.from_hex("100")
|
||||||
self.assertTrue(f"MessageId is {SnowflakeId.SNOWFLAKE_ID_WIDTH * 8} bits wide, it is represented by HEX string "
|
self.assertTrue(
|
||||||
in str(context.exception))
|
f"MessageId is {SnowflakeId.SNOWFLAKE_ID_WIDTH * 8} bits wide, it is represented by HEX string "
|
||||||
|
in str(context.exception)
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_bytes(self):
|
def test_get_bytes(self):
|
||||||
_factory = DataMessageFactory.get_instance(1)
|
_factory = DataMessageFactory.get_instance(1)
|
||||||
@@ -41,7 +42,6 @@ class SnowflakeIdTest(TestCase):
|
|||||||
_id_1 = _factory.generate_snowflake_id()
|
_id_1 = _factory.generate_snowflake_id()
|
||||||
self.assertEqual(2, len(_id_1.get_bits()))
|
self.assertEqual(2, len(_id_1.get_bits()))
|
||||||
|
|
||||||
|
|
||||||
def test_as_string(self):
|
def test_as_string(self):
|
||||||
_factory = DataMessageFactory.get_instance(1)
|
_factory = DataMessageFactory.get_instance(1)
|
||||||
_id_1 = _factory.generate_snowflake_id()
|
_id_1 = _factory.generate_snowflake_id()
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from pika.exchange_type import ExchangeType
|
|||||||
from amqp.adapter.data_message_factory import DataMessageFactory
|
from amqp.adapter.data_message_factory import DataMessageFactory
|
||||||
from amqp.adapter.logging_utils import logging_error
|
from amqp.adapter.logging_utils import logging_error
|
||||||
from amqp.model.model import DataMessage, AMQRoute
|
from amqp.model.model import DataMessage, AMQRoute
|
||||||
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE, DLQ_EXCHANGE, AMQ_RPC_EXCHANGE
|
from amqp.router.router_base import (
|
||||||
|
AMQ_REPLY_TO_EXCHANGE,
|
||||||
|
DLQ_EXCHANGE,
|
||||||
|
AMQ_RPC_EXCHANGE,
|
||||||
|
)
|
||||||
from amqp.router.router_consumer import RouterConsumer
|
from amqp.router.router_consumer import RouterConsumer
|
||||||
from amqp.router.utils import sanitize_as_rabbitmq_name
|
from amqp.router.utils import sanitize_as_rabbitmq_name
|
||||||
|
|
||||||
@@ -23,15 +27,20 @@ class RabbitMQClient:
|
|||||||
self.reply_exchange: aio_pika.RobustExchange | None = None
|
self.reply_exchange: aio_pika.RobustExchange | None = None
|
||||||
self.amq_configuration = configuration
|
self.amq_configuration = configuration
|
||||||
logging.info("*******************************************")
|
logging.info("*******************************************")
|
||||||
logging.info("******** RabbitMQProducer.init(): %s",
|
logging.info(
|
||||||
self.amq_configuration.dispatch.amq_host)
|
"******** RabbitMQProducer.init(): %s",
|
||||||
|
self.amq_configuration.dispatch.amq_host,
|
||||||
|
)
|
||||||
logging.info("*******************************************")
|
logging.info("*******************************************")
|
||||||
self.router = RouterConsumer(configuration)
|
self.router = RouterConsumer(configuration)
|
||||||
# this.meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
|
# this.meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
|
||||||
|
|
||||||
|
self.reply_to_exchange: AbstractRobustExchange | None = (
|
||||||
self.reply_to_exchange: AbstractRobustExchange | None = None # when sending response to RPC request
|
None # when sending response to RPC request
|
||||||
self.reply_to_queue: AbstractRobustQueue | None= None # when receiving response to RPC request
|
)
|
||||||
|
self.reply_to_queue: AbstractRobustQueue | None = (
|
||||||
|
None # when receiving response to RPC request
|
||||||
|
)
|
||||||
|
|
||||||
async def init(self, loop) -> AbstractRobustExchange:
|
async def init(self, loop) -> AbstractRobustExchange:
|
||||||
await self.setup_amq_channel(loop)
|
await self.setup_amq_channel(loop)
|
||||||
@@ -43,7 +52,9 @@ class RabbitMQClient:
|
|||||||
self.reply_to_exchange = await self.channel.declare_exchange(
|
self.reply_to_exchange = await self.channel.declare_exchange(
|
||||||
AMQ_REPLY_TO_EXCHANGE, type=ExchangeType.direct
|
AMQ_REPLY_TO_EXCHANGE, type=ExchangeType.direct
|
||||||
)
|
)
|
||||||
await self.reply_to_queue.bind(exchange=self.reply_to_exchange, routing_key=cm_reply_to_queue)
|
await self.reply_to_queue.bind(
|
||||||
|
exchange=self.reply_to_exchange, routing_key=cm_reply_to_queue
|
||||||
|
)
|
||||||
return self.reply_to_exchange
|
return self.reply_to_exchange
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
@@ -55,7 +66,10 @@ class RabbitMQClient:
|
|||||||
if not self.connection.is_closed:
|
if not self.connection.is_closed:
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(" [E] RabbitMQClient.PreDestroy cleanup - connection close error: %s", e)
|
logging_error(
|
||||||
|
" [E] RabbitMQClient.PreDestroy cleanup - connection close error: %s",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
async def register_inbound_routes(self, route_mapping: str, callback):
|
async def register_inbound_routes(self, route_mapping: str, callback):
|
||||||
"""
|
"""
|
||||||
@@ -66,36 +80,58 @@ class RabbitMQClient:
|
|||||||
After this the Service is listening on (consuming from) the queue for incoming messages.
|
After this the Service is listening on (consuming from) the queue for incoming messages.
|
||||||
"""
|
"""
|
||||||
if self.router.is_open(self.channel):
|
if self.router.is_open(self.channel):
|
||||||
queue_args = self.DLQ_ARGS if self.amq_configuration.dispatch.use_dlq else None
|
queue_args = (
|
||||||
logging.info(" [*] RabbitMQClient register routes, cfg mapping: [%s]", route_mapping)
|
self.DLQ_ARGS if self.amq_configuration.dispatch.use_dlq else None
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [*] RabbitMQClient register routes, cfg mapping: [%s]", route_mapping
|
||||||
|
)
|
||||||
# convert the comma-separated string into list of AMQRoute objects
|
# convert the comma-separated string into list of AMQRoute objects
|
||||||
requested_routes = self.router.unwrap_route_list(route_mapping)
|
requested_routes = self.router.unwrap_route_list(route_mapping)
|
||||||
for route in requested_routes:
|
for route in requested_routes:
|
||||||
try:
|
try:
|
||||||
# Note: AMQRoute allows one of the exchange or queue to be None, in which case the default is used
|
# Note: AMQRoute allows one of the exchange or queue to be None, in which case the default is used
|
||||||
queue_name = route.queue if route.queue else self.router.get_consume_queue_name()
|
queue_name = (
|
||||||
|
route.queue
|
||||||
|
if route.queue
|
||||||
|
else self.router.get_consume_queue_name()
|
||||||
|
)
|
||||||
logging.info(" [*] RabbitMQClient register route: [%s]", route)
|
logging.info(" [*] RabbitMQClient register route: [%s]", route)
|
||||||
queue: AbstractRobustQueue = await self.channel.declare_queue(name=queue_name,
|
queue: AbstractRobustQueue = await self.channel.declare_queue(
|
||||||
durable=True,
|
name=queue_name,
|
||||||
exclusive=False,
|
durable=True,
|
||||||
auto_delete=False,
|
exclusive=False,
|
||||||
arguments=queue_args)
|
auto_delete=False,
|
||||||
_exchange_name: str = route.exchange if route.exchange else AMQ_RPC_EXCHANGE
|
arguments=queue_args,
|
||||||
exchange: AbstractRobustExchange = await self.channel.declare_exchange(
|
)
|
||||||
name=_exchange_name, type=ExchangeType.topic
|
_exchange_name: str = (
|
||||||
|
route.exchange if route.exchange else AMQ_RPC_EXCHANGE
|
||||||
|
)
|
||||||
|
exchange: AbstractRobustExchange = (
|
||||||
|
await self.channel.declare_exchange(
|
||||||
|
name=_exchange_name, type=ExchangeType.topic
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await queue.bind(exchange, route.key)
|
await queue.bind(exchange, route.key)
|
||||||
_c_tag = await queue.consume(callback, no_ack=False)
|
_c_tag = await queue.consume(callback, no_ack=False)
|
||||||
# Register the route with the router and publish its detail to other adapters
|
# Register the route with the router and publish its detail to other adapters
|
||||||
await self.router.register_route(
|
await self.router.register_route(
|
||||||
AMQRoute(
|
AMQRoute(
|
||||||
route.component_name, _exchange_name, queue_name,
|
route.component_name,
|
||||||
|
_exchange_name,
|
||||||
|
queue_name,
|
||||||
sanitize_as_rabbitmq_name(route.key),
|
sanitize_as_rabbitmq_name(route.key),
|
||||||
route.timeout, route.valid_until
|
route.timeout,
|
||||||
|
route.valid_until,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logging.info(" [%s] AMQ connection initialized for route: %s, listens on: %s/%s",
|
logging.info(
|
||||||
_c_tag, route, exchange, queue_name)
|
" [%s] AMQ connection initialized for route: %s, listens on: %s/%s",
|
||||||
|
_c_tag,
|
||||||
|
route,
|
||||||
|
exchange,
|
||||||
|
queue_name,
|
||||||
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logging_error("Callback registration INCOMPLETE, %s", err)
|
logging_error("Callback registration INCOMPLETE, %s", err)
|
||||||
else:
|
else:
|
||||||
@@ -103,7 +139,11 @@ class RabbitMQClient:
|
|||||||
|
|
||||||
async def register_data_reply_callback(self, rpc_callback):
|
async def register_data_reply_callback(self, rpc_callback):
|
||||||
_c_tag = await self.reply_to_queue.consume(callback=rpc_callback, no_ack=False)
|
_c_tag = await self.reply_to_queue.consume(callback=rpc_callback, no_ack=False)
|
||||||
logging.info(" [%s] RabbitMQClient register data reply callback on queue: %s", _c_tag,self.reply_to_queue.name)
|
logging.info(
|
||||||
|
" [%s] RabbitMQClient register data reply callback on queue: %s",
|
||||||
|
_c_tag,
|
||||||
|
self.reply_to_queue.name,
|
||||||
|
)
|
||||||
|
|
||||||
async def setup_amq_channel(self, loop):
|
async def setup_amq_channel(self, loop):
|
||||||
"""
|
"""
|
||||||
@@ -115,15 +155,21 @@ class RabbitMQClient:
|
|||||||
port=self.amq_configuration.dispatch.amq_port,
|
port=self.amq_configuration.dispatch.amq_port,
|
||||||
login=self.amq_configuration.dispatch.rabbit_mq_user,
|
login=self.amq_configuration.dispatch.rabbit_mq_user,
|
||||||
password=self.amq_configuration.dispatch.rabbit_mq_password,
|
password=self.amq_configuration.dispatch.rabbit_mq_password,
|
||||||
loop=loop
|
loop=loop,
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
"RabbitMQProducer.setupAMQ, got connection, open=%s",
|
||||||
|
self.connection.is_closed,
|
||||||
)
|
)
|
||||||
logging.info("RabbitMQProducer.setupAMQ, got connection, open=%s", self.connection.is_closed)
|
|
||||||
self.channel = await self.connection.channel()
|
self.channel = await self.connection.channel()
|
||||||
await self.channel.set_qos(prefetch_count=self.PREFETCH_COUNT)
|
await self.channel.set_qos(prefetch_count=self.PREFETCH_COUNT)
|
||||||
await self.router.init_internal_routing_paths(self.channel)
|
await self.router.init_internal_routing_paths(self.channel)
|
||||||
self.router.set_route_added_notifier(
|
self.router.set_route_added_notifier(
|
||||||
lambda route: logging.info(" [DBG] ---------- RouteAdded, setting up metrics: amqp.adapter.exchange.%s",
|
lambda route: logging.info(
|
||||||
route.exchange))
|
" [DBG] ---------- RouteAdded, setting up metrics: amqp.adapter.exchange.%s",
|
||||||
|
route.exchange,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def send_message(self, message: DataMessage):
|
async def send_message(self, message: DataMessage):
|
||||||
"""
|
"""
|
||||||
@@ -140,31 +186,42 @@ class RabbitMQClient:
|
|||||||
# No response expected
|
# No response expected
|
||||||
pika_message: aio_pika.message.Message = aio_pika.message.Message(
|
pika_message: aio_pika.message.Message = aio_pika.message.Message(
|
||||||
body=DataMessageFactory.serialize(message),
|
body=DataMessageFactory.serialize(message),
|
||||||
content_encoding='utf-8',
|
content_encoding="utf-8",
|
||||||
delivery_mode=2
|
delivery_mode=2,
|
||||||
)
|
)
|
||||||
await self.rpc_exchange.publish(
|
await self.rpc_exchange.publish(
|
||||||
message=pika_message,
|
message=pika_message,
|
||||||
routing_key=self.router.get_routing_key(message), # context path is a routing key
|
routing_key=self.router.get_routing_key(
|
||||||
|
message
|
||||||
|
), # context path is a routing key
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
f" [x] basicPublish (no-reply), messageId: {message.id}, to: {route}"
|
||||||
)
|
)
|
||||||
logging.info(f" [x] basicPublish (no-reply), messageId: {message.id}, to: {route}")
|
|
||||||
else:
|
else:
|
||||||
# This is RPC call, there should be response
|
# This is RPC call, there should be response
|
||||||
pika_message: aio_pika.message.Message = aio_pika.message.Message(
|
pika_message: aio_pika.message.Message = aio_pika.message.Message(
|
||||||
body=DataMessageFactory.serialize(message),
|
body=DataMessageFactory.serialize(message),
|
||||||
content_encoding='utf-8',
|
content_encoding="utf-8",
|
||||||
delivery_mode=2,
|
delivery_mode=2,
|
||||||
correlation_id=str(message.id),
|
correlation_id=str(message.id),
|
||||||
reply_to=self.router.get_reply_to_queue_name()
|
reply_to=self.router.get_reply_to_queue_name(),
|
||||||
)
|
)
|
||||||
await self.rpc_exchange.publish(
|
await self.rpc_exchange.publish(
|
||||||
message=pika_message,
|
message=pika_message,
|
||||||
routing_key=self.router.get_routing_key(message), # context path is a routing key
|
routing_key=self.router.get_routing_key(
|
||||||
|
message
|
||||||
|
), # context path is a routing key
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [x] basicPublish (RPC call), messageId: %s, to: %s",
|
||||||
|
message.id.as_string(),
|
||||||
|
route.as_string(),
|
||||||
)
|
)
|
||||||
logging.info(" [x] basicPublish (RPC call), messageId: %s, to: %s",
|
|
||||||
message.id.as_string(), route.as_string())
|
|
||||||
else:
|
else:
|
||||||
logging.warning("Message not sent, no route for %s/%s", message.domain, message.path)
|
logging.warning(
|
||||||
|
"Message not sent, no route for %s/%s", message.domain, message.path
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logging_error("Send message failed: %s", err)
|
logging_error("Send message failed: %s", err)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class RouteDatabase:
|
|||||||
|
|
||||||
def pattern(self, routing_key: str) -> re.Pattern:
|
def pattern(self, routing_key: str) -> re.Pattern:
|
||||||
breaker = False
|
breaker = False
|
||||||
tokens = routing_key.split('.')
|
tokens = routing_key.split(".")
|
||||||
counter = len(tokens)
|
counter = len(tokens)
|
||||||
regex_parts = []
|
regex_parts = []
|
||||||
|
|
||||||
@@ -40,9 +40,13 @@ class RouteDatabase:
|
|||||||
return re.compile(".*")
|
return re.compile(".*")
|
||||||
return re.compile(regex)
|
return re.compile(regex)
|
||||||
|
|
||||||
def matches(self, routing_key_from_route: str, routing_key_from_message: str) -> bool:
|
def matches(
|
||||||
|
self, routing_key_from_route: str, routing_key_from_message: str
|
||||||
|
) -> bool:
|
||||||
route_pattern = self.pattern(routing_key_from_route)
|
route_pattern = self.pattern(routing_key_from_route)
|
||||||
return bool(route_pattern.match(sanitize_as_rabbitmq_name(routing_key_from_message)))
|
return bool(
|
||||||
|
route_pattern.match(sanitize_as_rabbitmq_name(routing_key_from_message))
|
||||||
|
)
|
||||||
|
|
||||||
def wrap_routes(self) -> str:
|
def wrap_routes(self) -> str:
|
||||||
return ",".join(str(route) for route in self.routes)
|
return ",".join(str(route) for route in self.routes)
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ class RouterBase:
|
|||||||
|
|
||||||
def unwrap_route_list(self, route_string: str) -> List[AMQRoute]:
|
def unwrap_route_list(self, route_string: str) -> List[AMQRoute]:
|
||||||
logging.info(" [DBG] RouterMaster.unwrapRoutes, route(s): %s", route_string)
|
logging.info(" [DBG] RouterMaster.unwrapRoutes, route(s): %s", route_string)
|
||||||
routes = [AMQRouteFactory.from_string(route_str) for route_str in route_string.split(",")]
|
routes = [
|
||||||
|
AMQRouteFactory.from_string(route_str)
|
||||||
|
for route_str in route_string.split(",")
|
||||||
|
]
|
||||||
return [route for route in routes if route != NULL_ROUTE]
|
return [route for route in routes if route != NULL_ROUTE]
|
||||||
|
|
||||||
def get_routes(self) -> Set[AMQRoute]:
|
def get_routes(self) -> Set[AMQRoute]:
|
||||||
@@ -53,13 +56,19 @@ class RouterBase:
|
|||||||
def add_routes(self, message: str) -> None:
|
def add_routes(self, message: str) -> None:
|
||||||
try:
|
try:
|
||||||
amq_route_list = self.unwrap_route_list(message)
|
amq_route_list = self.unwrap_route_list(message)
|
||||||
logging.info(" [DBG] RouterMaster.addRoute [%d]: %s", len(amq_route_list), message)
|
logging.info(
|
||||||
|
" [DBG] RouterMaster.addRoute [%d]: %s", len(amq_route_list), message
|
||||||
|
)
|
||||||
for route in amq_route_list:
|
for route in amq_route_list:
|
||||||
logging.info(" [*] RouterMaster.addRoute, adding route: %s", route)
|
logging.info(" [*] RouterMaster.addRoute, adding route: %s", route)
|
||||||
if not route.exchange and not route.queue and not route.key:
|
if not route.exchange and not route.queue and not route.key:
|
||||||
logging_error("Cannot setup AMQ Exchange for '%s', at least one of "
|
logging_error(
|
||||||
"Exchange, Queue and/or Key must be present in the routing string. "
|
"Cannot setup AMQ Exchange for '%s', at least one of "
|
||||||
"Expected format is '%s'", route, AMQRoute.FORMAT)
|
"Exchange, Queue and/or Key must be present in the routing string. "
|
||||||
|
"Expected format is '%s'",
|
||||||
|
route,
|
||||||
|
AMQRoute.FORMAT,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if route.exchange != DLQ_EXCHANGE:
|
if route.exchange != DLQ_EXCHANGE:
|
||||||
is_new_route = self.route_database.add_route(route)
|
is_new_route = self.route_database.add_route(route)
|
||||||
@@ -73,9 +82,14 @@ class RouterBase:
|
|||||||
|
|
||||||
def remove_routes(self, message: str) -> None:
|
def remove_routes(self, message: str) -> None:
|
||||||
try:
|
try:
|
||||||
to_remove = [AMQRouteFactory.from_string(route_str) for route_str in message.split(",")]
|
to_remove = [
|
||||||
|
AMQRouteFactory.from_string(route_str)
|
||||||
|
for route_str in message.split(",")
|
||||||
|
]
|
||||||
removed_count = sum(1 for route in to_remove if self.remove_route(route))
|
removed_count = sum(1 for route in to_remove if self.remove_route(route))
|
||||||
logging.info(f" [*] RouterMaster.removeRoutes(): {message}, removed={removed_count}")
|
logging.info(
|
||||||
|
f" [*] RouterMaster.removeRoutes(): {message}, removed={removed_count}"
|
||||||
|
)
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
logging.info(f" [E] Can't remove route(s): {err}")
|
logging.info(f" [E] Can't remove route(s): {err}")
|
||||||
|
|
||||||
|
|||||||
+123
-51
@@ -1,27 +1,36 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from aio_pika.abc import AbstractRobustChannel, AbstractRobustQueue, AbstractIncomingMessage
|
from aio_pika.abc import (
|
||||||
|
AbstractRobustChannel,
|
||||||
|
AbstractRobustQueue,
|
||||||
|
AbstractIncomingMessage,
|
||||||
|
)
|
||||||
from amqp.adapter.amq_route_factory import AMQRouteFactory
|
from amqp.adapter.amq_route_factory import AMQRouteFactory
|
||||||
from amqp.adapter.logging_utils import logging_error
|
from amqp.adapter.logging_utils import logging_error
|
||||||
from amqp.config.amq_configuration import AMQConfiguration
|
from amqp.config.amq_configuration import AMQConfiguration
|
||||||
from amqp.model.model import ServiceMessage, AMQRoute
|
from amqp.model.model import ServiceMessage, AMQRoute
|
||||||
from amqp.model.service_message_type import ServiceMessageType
|
from amqp.model.service_message_type import ServiceMessageType
|
||||||
from amqp.router.router_base import CM_C_REQ_QUEUE, CM_REQUEST_EXCHANGE, CM_RESPONSE_EXCHANGE, ANY_RECIPIENT
|
from amqp.router.router_base import (
|
||||||
|
CM_C_REQ_QUEUE,
|
||||||
|
CM_REQUEST_EXCHANGE,
|
||||||
|
CM_RESPONSE_EXCHANGE,
|
||||||
|
ANY_RECIPIENT,
|
||||||
|
)
|
||||||
from amqp.router.router_producer import RouterProducer
|
from amqp.router.router_producer import RouterProducer
|
||||||
|
|
||||||
|
|
||||||
class RouterConsumer(RouterProducer):
|
class RouterConsumer(RouterProducer):
|
||||||
"""
|
"""
|
||||||
* Contains Consumer logic. The logic here:
|
* Contains Consumer logic. The logic here:
|
||||||
* Sets a listener on CM_C_REQ_QUEUE queue to receive routing data requests
|
* Sets a listener on CM_C_REQ_QUEUE queue to receive routing data requests
|
||||||
* Upon receipt of routing data request, responds on CM_RESPONSE_EXCHANGE with local routing data.
|
* Upon receipt of routing data request, responds on CM_RESPONSE_EXCHANGE with local routing data.
|
||||||
* It also proactively pushes the local routing data to CM_RESPONSE_EXCHANGE on startup to
|
* It also proactively pushes the local routing data to CM_RESPONSE_EXCHANGE on startup to
|
||||||
* notify the other (excluding self) Producers about its presence.
|
* notify the other (excluding self) Producers about its presence.
|
||||||
*
|
*
|
||||||
* Consumer is also Producer (Consumer sits alongside the CM Service, and anytime the Service needs
|
* Consumer is also Producer (Consumer sits alongside the CM Service, and anytime the Service needs
|
||||||
* to communicate with other Service, it can do so via the Adapter). Therefore, the Producer
|
* to communicate with other Service, it can do so via the Adapter). Therefore, the Producer
|
||||||
* capability is also required.
|
* capability is also required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, configuration: AMQConfiguration):
|
def __init__(self, configuration: AMQConfiguration):
|
||||||
@@ -31,94 +40,148 @@ class RouterConsumer(RouterProducer):
|
|||||||
|
|
||||||
async def init_routing_paths_consumer(self, _channel: AbstractRobustChannel):
|
async def init_routing_paths_consumer(self, _channel: AbstractRobustChannel):
|
||||||
"""
|
"""
|
||||||
* Initialize router according to Consumer mode.
|
* Initialize router according to Consumer mode.
|
||||||
"""
|
"""
|
||||||
logging.info(" [*] RouterConsumer.initConsumerRouter, channel open: %s", self.is_open(_channel))
|
logging.info(
|
||||||
|
" [*] RouterConsumer.initConsumerRouter, channel open: %s",
|
||||||
|
self.is_open(_channel),
|
||||||
|
)
|
||||||
if self.is_open(_channel):
|
if self.is_open(_channel):
|
||||||
try:
|
try:
|
||||||
# 1. declare a Queue to receive the RoutingData requests
|
# 1. declare a Queue to receive the RoutingData requests
|
||||||
_cm_request_queue: str = self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_REQ_QUEUE
|
_cm_request_queue: str = (
|
||||||
_queue: AbstractRobustQueue = await self.channel.declare_queue(name=_cm_request_queue, durable=True, exclusive=False, auto_delete=False)
|
self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_REQ_QUEUE
|
||||||
|
)
|
||||||
|
_queue: AbstractRobustQueue = await self.channel.declare_queue(
|
||||||
|
name=_cm_request_queue,
|
||||||
|
durable=True,
|
||||||
|
exclusive=False,
|
||||||
|
auto_delete=False,
|
||||||
|
)
|
||||||
# 1a. bind queue to CM_REQUEST_EXCHANGE w/o any conditions (empty routing key)
|
# 1a. bind queue to CM_REQUEST_EXCHANGE w/o any conditions (empty routing key)
|
||||||
await _queue.bind(exchange=CM_REQUEST_EXCHANGE, routing_key="")
|
await _queue.bind(exchange=CM_REQUEST_EXCHANGE, routing_key="")
|
||||||
# 1b. Service adapter mode - listens on Request queue and replies to Response queue
|
# 1b. Service adapter mode - listens on Request queue and replies to Response queue
|
||||||
_c_tag = await _queue.consume(callback=self.handle_routing_data_request_message, no_ack=False)
|
_c_tag = await _queue.consume(
|
||||||
logging.info(" [%s] RouterConsumer listens for RouteDataRequests on: %s", _c_tag, _cm_request_queue)
|
callback=self.handle_routing_data_request_message, no_ack=False
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [%s] RouterConsumer listens for RouteDataRequests on: %s",
|
||||||
|
_c_tag,
|
||||||
|
_cm_request_queue,
|
||||||
|
)
|
||||||
except Exception as ioe:
|
except Exception as ioe:
|
||||||
logging_error("RouterConsumer FATAL: cannot initialize, %s", ioe)
|
logging_error("RouterConsumer FATAL: cannot initialize, %s", ioe)
|
||||||
else:
|
else:
|
||||||
logging_error("RouterConsumer FATAL: cannot initialize, RabbitMQ channel is not open.")
|
logging_error(
|
||||||
|
"RouterConsumer FATAL: cannot initialize, RabbitMQ channel is not open."
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
* ServiceMessageType.ROUTING_DATA_REQUEST handler listening on queue 'CleverMicroRequest'
|
* ServiceMessageType.ROUTING_DATA_REQUEST handler listening on queue 'CleverMicroRequest'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def handle_routing_data_request_message(self, message: AbstractIncomingMessage):
|
async def handle_routing_data_request_message(
|
||||||
|
self, message: AbstractIncomingMessage
|
||||||
|
):
|
||||||
# some debug statements, TODO - remove for prod release
|
# some debug statements, TODO - remove for prod release
|
||||||
delivery_tag = message.delivery_tag
|
delivery_tag = message.delivery_tag
|
||||||
debug = "Delivery.Properties = " + message.properties.__str__()
|
debug = "Delivery.Properties = " + message.properties.__str__()
|
||||||
logging.info(" [DBG] ******[%s] (ROUTING_DATA_REQ) tag: %s, env: %s, props: %s",
|
logging.info(
|
||||||
delivery_tag, message.consumer_tag, message.properties, debug)
|
" [DBG] ******[%s] (ROUTING_DATA_REQ) tag: %s, env: %s, props: %s",
|
||||||
|
delivery_tag,
|
||||||
|
message.consumer_tag,
|
||||||
|
message.properties,
|
||||||
|
debug,
|
||||||
|
)
|
||||||
|
|
||||||
await message.ack(False)
|
await message.ack(False)
|
||||||
|
|
||||||
cm_message: ServiceMessage = self.service_message_factory.from_bytes(message.body)
|
cm_message: ServiceMessage = self.service_message_factory.from_bytes(
|
||||||
|
message.body
|
||||||
|
)
|
||||||
route_mapping: str = self.wrap_own_routes()
|
route_mapping: str = self.wrap_own_routes()
|
||||||
logging.info(" [DBG] ******[%s] (ROUTING_DATA_REQ) msg=%s, routeMap=[%s]",
|
logging.info(
|
||||||
delivery_tag, self.service_message_factory.to_string(cm_message), route_mapping)
|
" [DBG] ******[%s] (ROUTING_DATA_REQ) msg=%s, routeMap=[%s]",
|
||||||
|
delivery_tag,
|
||||||
|
self.service_message_factory.to_string(cm_message),
|
||||||
|
route_mapping,
|
||||||
|
)
|
||||||
# Reply only if self adapter has own routeMapping (e.g. API-gateway-side adapter has none)
|
# Reply only if self adapter has own routeMapping (e.g. API-gateway-side adapter has none)
|
||||||
if cm_message.is_valid() and cm_message.message_type == ServiceMessageType.ROUTING_DATA_REQUEST and route_mapping:
|
if (
|
||||||
|
cm_message.is_valid()
|
||||||
|
and cm_message.message_type == ServiceMessageType.ROUTING_DATA_REQUEST
|
||||||
|
and route_mapping
|
||||||
|
):
|
||||||
if cm_message.reply_to != self.amq_configuration.amq_adapter.service_name:
|
if cm_message.reply_to != self.amq_configuration.amq_adapter.service_name:
|
||||||
try:
|
try:
|
||||||
service_message: ServiceMessage = self.service_message_factory.of(
|
service_message: ServiceMessage = self.service_message_factory.of(
|
||||||
ServiceMessageType.ROUTING_DATA, route_mapping, cm_message.reply_to
|
ServiceMessageType.ROUTING_DATA,
|
||||||
|
route_mapping,
|
||||||
|
cm_message.reply_to,
|
||||||
|
)
|
||||||
|
await self.publish_service_message(
|
||||||
|
service_message, self.cm_response_exchange
|
||||||
)
|
)
|
||||||
await self.publish_service_message(service_message, self.cm_response_exchange)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logging_error("******[%s] (ROUTING_DATA_REQ) Response to queue: %s failed: %s",
|
logging_error(
|
||||||
delivery_tag, CM_RESPONSE_EXCHANGE, err)
|
"******[%s] (ROUTING_DATA_REQ) Response to queue: %s failed: %s",
|
||||||
|
delivery_tag,
|
||||||
|
CM_RESPONSE_EXCHANGE,
|
||||||
|
err,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.info(" [*] ******[%s] (ROUTING_DATA_REQ) [%s] Ignore request originated by ourselves.")
|
logging.info(
|
||||||
|
" [*] ******[%s] (ROUTING_DATA_REQ) [%s] Ignore request originated by ourselves."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging_error("******[%s] (ROUTING_DATA_REQ) [%s] produces invalid or no CM msg type.",
|
logging_error(
|
||||||
delivery_tag, str(message.body))
|
"******[%s] (ROUTING_DATA_REQ) [%s] produces invalid or no CM msg type.",
|
||||||
|
delivery_tag,
|
||||||
|
str(message.body),
|
||||||
|
)
|
||||||
|
|
||||||
async def register_route(self, route: AMQRoute):
|
async def register_route(self, route: AMQRoute):
|
||||||
"""
|
"""
|
||||||
* Called in Consumer (Service Adapter) context. Registers the routing data for self Service
|
* Called in Consumer (Service Adapter) context. Registers the routing data for self Service
|
||||||
* with the Master router by sending own routing data to Master via CleverMicroResponse queue.
|
* with the Master router by sending own routing data to Master via CleverMicroResponse queue.
|
||||||
* :param route: route to register with Master
|
* :param route: route to register with Master
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.is_open(self.channel) and route.queue:
|
if self.is_open(self.channel) and route.queue:
|
||||||
serviceMessage: ServiceMessage = self.service_message_factory.of(
|
serviceMessage: ServiceMessage = self.service_message_factory.of(
|
||||||
ServiceMessageType.ROUTING_DATA, route.as_string(), ANY_RECIPIENT
|
ServiceMessageType.ROUTING_DATA, route.as_string(), ANY_RECIPIENT
|
||||||
)
|
)
|
||||||
await self.publish_service_message(serviceMessage, self.cm_response_exchange)
|
await self.publish_service_message(
|
||||||
|
serviceMessage, self.cm_response_exchange
|
||||||
|
)
|
||||||
self.add_own_route(route)
|
self.add_own_route(route)
|
||||||
logging.info(" [DBG] RouterConsumer registered Route: %s", route)
|
logging.info(" [DBG] RouterConsumer registered Route: %s", route)
|
||||||
else:
|
else:
|
||||||
logging.warning(" [W] RouterConsumer couldn't register route data: %s channel isn't open.",
|
logging.warning(
|
||||||
CM_RESPONSE_EXCHANGE)
|
" [W] RouterConsumer couldn't register route data: %s channel isn't open.",
|
||||||
|
CM_RESPONSE_EXCHANGE,
|
||||||
|
)
|
||||||
except Exception as ioe:
|
except Exception as ioe:
|
||||||
logging_error("RouterConsumer failed register route data: %s", ioe)
|
logging_error("RouterConsumer failed register route data: %s", ioe)
|
||||||
|
|
||||||
async def remove_route_on_cleanup(self, route: AMQRoute) -> bool:
|
async def remove_route_on_cleanup(self, route: AMQRoute) -> bool:
|
||||||
"""
|
"""
|
||||||
* Unregister / remove route from the list
|
* Unregister / remove route from the list
|
||||||
* :param route: route to remove
|
* :param route: route to remove
|
||||||
* :return result of remove operation
|
* :return result of remove operation
|
||||||
"""
|
"""
|
||||||
logging.info(" [DBG] RouterConsumer.removeRoute, route: %s", route.as_string())
|
logging.info(" [DBG] RouterConsumer.removeRoute, route: %s", route.as_string())
|
||||||
try:
|
try:
|
||||||
service_message: ServiceMessage = self.service_message_factory.of(
|
service_message: ServiceMessage = self.service_message_factory.of(
|
||||||
ServiceMessageType.ROUTING_DATA_REMOVE,
|
ServiceMessageType.ROUTING_DATA_REMOVE, route.as_string(), ANY_RECIPIENT
|
||||||
route.as_string(),
|
|
||||||
ANY_RECIPIENT
|
|
||||||
)
|
)
|
||||||
if not await self.publish_service_message(service_message, self.cm_response_exchange):
|
if not await self.publish_service_message(
|
||||||
logging.warning(" [W] RouterConsumer couldn't remove current route: %s, channel not open.",
|
service_message, self.cm_response_exchange
|
||||||
route)
|
):
|
||||||
|
logging.warning(
|
||||||
|
" [W] RouterConsumer couldn't remove current route: %s, channel not open.",
|
||||||
|
route,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as ioe:
|
except Exception as ioe:
|
||||||
logging_error("RouterConsumer failed remove current route: %s", ioe)
|
logging_error("RouterConsumer failed remove current route: %s", ioe)
|
||||||
@@ -128,10 +191,19 @@ class RouterConsumer(RouterProducer):
|
|||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
* Cleanup - de-register routes with master.
|
* Cleanup - de-register routes with master.
|
||||||
"""
|
"""
|
||||||
routes: Set[AMQRoute] = self.get_routes()
|
routes: Set[AMQRoute] = self.get_routes()
|
||||||
logging.info(" [DBG] RouterConsumer.cleanup() [%d]: %s", len(routes), self.wrap_routes())
|
logging.info(
|
||||||
not_removed = sum(1 for r in (self.remove_route_on_cleanup(route) for route in routes) if not r)
|
" [DBG] RouterConsumer.cleanup() [%d]: %s", len(routes), self.wrap_routes()
|
||||||
|
)
|
||||||
|
not_removed = sum(
|
||||||
|
1
|
||||||
|
for r in (self.remove_route_on_cleanup(route) for route in routes)
|
||||||
|
if not r
|
||||||
|
)
|
||||||
if not_removed > 0:
|
if not_removed > 0:
|
||||||
logging.info(" [W] %d route(s) not removed. self may lead to failed delivery on self route", not_removed)
|
logging.info(
|
||||||
|
" [W] %d route(s) not removed. self may lead to failed delivery on self route",
|
||||||
|
not_removed,
|
||||||
|
)
|
||||||
|
|||||||
+133
-76
@@ -1,16 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
* Maps the request to RabbitMQ exchange. Handles the service discovery logic.
|
* Maps the request to RabbitMQ exchange. Handles the service discovery logic.
|
||||||
*
|
*
|
||||||
* Router can be either in 'Master' or 'Service (Consumer)' modes. 'Master' mode is the outward
|
* Router can be either in 'Master' or 'Service (Consumer)' modes. 'Master' mode is the outward
|
||||||
* facing side that receives the requests and routes it to queue associated with requested service.
|
* facing side that receives the requests and routes it to queue associated with requested service.
|
||||||
* 'Service' mode is consumer side for given service, which receives the message from AMQ and
|
* 'Service' mode is consumer side for given service, which receives the message from AMQ and
|
||||||
* forwards it to the application.
|
* forwards it to the application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aio_pika import Message
|
from aio_pika import Message
|
||||||
from aio_pika.abc import AbstractRobustChannel, AbstractRobustQueue, AbstractRobustConnection, AbstractRobustExchange, \
|
from aio_pika.abc import (
|
||||||
AbstractIncomingMessage
|
AbstractRobustChannel,
|
||||||
|
AbstractRobustQueue,
|
||||||
|
AbstractRobustConnection,
|
||||||
|
AbstractRobustExchange,
|
||||||
|
AbstractIncomingMessage,
|
||||||
|
)
|
||||||
from pika.exchange_type import ExchangeType
|
from pika.exchange_type import ExchangeType
|
||||||
|
|
||||||
from amqp.adapter.logging_utils import logging_error
|
from amqp.adapter.logging_utils import logging_error
|
||||||
@@ -18,40 +24,54 @@ from amqp.adapter.service_message_factory import ServiceMessageFactory
|
|||||||
from amqp.config.amq_configuration import AMQConfiguration
|
from amqp.config.amq_configuration import AMQConfiguration
|
||||||
from amqp.model.model import ServiceMessage
|
from amqp.model.model import ServiceMessage
|
||||||
from amqp.model.service_message_type import ServiceMessageType
|
from amqp.model.service_message_type import ServiceMessageType
|
||||||
from amqp.router.router_base import RouterBase, CM_REQUEST_EXCHANGE, CM_RESPONSE_EXCHANGE, CM_C_AMQ_QUEUE, \
|
from amqp.router.router_base import (
|
||||||
CM_P_RESP_QUEUE, CM_P_REPLY_TO, ANY_RECIPIENT
|
RouterBase,
|
||||||
|
CM_REQUEST_EXCHANGE,
|
||||||
|
CM_RESPONSE_EXCHANGE,
|
||||||
|
CM_C_AMQ_QUEUE,
|
||||||
|
CM_P_RESP_QUEUE,
|
||||||
|
CM_P_REPLY_TO,
|
||||||
|
ANY_RECIPIENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RouterProducer(RouterBase):
|
class RouterProducer(RouterBase):
|
||||||
|
|
||||||
def __init__(self, configuration: AMQConfiguration):
|
def __init__(self, configuration: AMQConfiguration):
|
||||||
"""
|
"""
|
||||||
* Initialize router - need to supply the config values and RabbitMQ channel.
|
* Initialize router - need to supply the config values and RabbitMQ channel.
|
||||||
* :param configuration: adapter's configuration reference.
|
* :param configuration: adapter's configuration reference.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.connection: AbstractRobustConnection | None = None
|
self.connection: AbstractRobustConnection | None = None
|
||||||
self.service_message_factory = ServiceMessageFactory(configuration.amq_adapter.service_name)
|
self.service_message_factory = ServiceMessageFactory(
|
||||||
|
configuration.amq_adapter.service_name
|
||||||
|
)
|
||||||
self.amq_configuration: AMQConfiguration = configuration
|
self.amq_configuration: AMQConfiguration = configuration
|
||||||
self.channel: AbstractRobustChannel | None = None
|
self.channel: AbstractRobustChannel | None = None
|
||||||
self.cm_request_exchange: AbstractRobustExchange | None = None
|
self.cm_request_exchange: AbstractRobustExchange | None = None
|
||||||
self.cm_response_exchange: AbstractRobustExchange | None = None
|
self.cm_response_exchange: AbstractRobustExchange | None = None
|
||||||
|
|
||||||
logging.info(f" [*] RouterProducer.<init>: %s, route: %s",
|
logging.info(
|
||||||
self.amq_configuration.amq_adapter.service_name,
|
f" [*] RouterProducer.<init>: %s, route: %s",
|
||||||
self.amq_configuration.amq_adapter.route_mapping)
|
self.amq_configuration.amq_adapter.service_name,
|
||||||
|
self.amq_configuration.amq_adapter.route_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
async def init_internal_routing_paths(self, channel: AbstractRobustChannel | None):
|
async def init_internal_routing_paths(self, channel: AbstractRobustChannel | None):
|
||||||
"""
|
"""
|
||||||
* Initialize internal routes for ServiceMessages.
|
* Initialize internal routes for ServiceMessages.
|
||||||
* The logic here sets a producer and listener on CleverMicroRequest/CleverMicroResponse queues
|
* The logic here sets a producer and listener on CleverMicroRequest/CleverMicroResponse queues
|
||||||
* created to enable the service discovery.
|
* created to enable the service discovery.
|
||||||
* The Producer mode sends out routing data request on CleverMicroRequest and listens
|
* The Producer mode sends out routing data request on CleverMicroRequest and listens
|
||||||
* on CleverMicroResponse for any service responding with its routing data.
|
* on CleverMicroResponse for any service responding with its routing data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
logging.info(" [*] RouterProducer.initRoutingPaths, channel open: %s", self.is_open(self.channel))
|
logging.info(
|
||||||
|
" [*] RouterProducer.initRoutingPaths, channel open: %s",
|
||||||
|
self.is_open(self.channel),
|
||||||
|
)
|
||||||
if self.is_open(self.channel):
|
if self.is_open(self.channel):
|
||||||
try:
|
try:
|
||||||
# **** set up the service discovery internal exchanges ****
|
# **** set up the service discovery internal exchanges ****
|
||||||
@@ -68,42 +88,68 @@ class RouterProducer(RouterBase):
|
|||||||
# 2a. declare a Response queue for Routing Data replies from Services
|
# 2a. declare a Response queue for Routing Data replies from Services
|
||||||
_cm_response_queue: str = self.get_response_queue_name()
|
_cm_response_queue: str = self.get_response_queue_name()
|
||||||
_response_queue: AbstractRobustQueue = await self.channel.declare_queue(
|
_response_queue: AbstractRobustQueue = await self.channel.declare_queue(
|
||||||
name=_cm_response_queue, durable=True, exclusive=False, auto_delete=False,
|
name=_cm_response_queue,
|
||||||
arguments={}
|
durable=True,
|
||||||
|
exclusive=False,
|
||||||
|
auto_delete=False,
|
||||||
|
arguments={},
|
||||||
)
|
)
|
||||||
# 2b. bind queue to CM_RESPONSE_EXCHANGE w/o any conditions (empty routing key)
|
# 2b. bind queue to CM_RESPONSE_EXCHANGE w/o any conditions (empty routing key)
|
||||||
await _response_queue.bind(exchange=CM_RESPONSE_EXCHANGE, routing_key="#")
|
await _response_queue.bind(
|
||||||
|
exchange=CM_RESPONSE_EXCHANGE, routing_key="#"
|
||||||
|
)
|
||||||
# 2c. listen for incoming RoutingData messages
|
# 2c. listen for incoming RoutingData messages
|
||||||
_c_tag = await _response_queue.consume(callback=self.handle_routing_data_message, no_ack=False)
|
_c_tag = await _response_queue.consume(
|
||||||
logging.info(" [%s] Routing master listens for Route updates on %s", _c_tag, _cm_response_queue)
|
callback=self.handle_routing_data_message, no_ack=False
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [%s] Routing master listens for Route updates on %s",
|
||||||
|
_c_tag,
|
||||||
|
_cm_response_queue,
|
||||||
|
)
|
||||||
except Exception as ioe:
|
except Exception as ioe:
|
||||||
logging_error("RouterProducer FATAL: cannot initialize, %s", ioe)
|
logging_error("RouterProducer FATAL: cannot initialize, %s", ioe)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging_error("RouterProducer FATAL: cannot initialize, RabbitMQ channel is not open.")
|
logging_error(
|
||||||
|
"RouterProducer FATAL: cannot initialize, RabbitMQ channel is not open."
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_routing_data_message(self, message: AbstractIncomingMessage):
|
async def handle_routing_data_message(self, message: AbstractIncomingMessage):
|
||||||
"""
|
"""
|
||||||
* Handler for CLEVER_MICRO_RESPONSE queue messages. Used in Master mode to handle receipt of
|
* Handler for CLEVER_MICRO_RESPONSE queue messages. Used in Master mode to handle receipt of
|
||||||
* routing data, invokes addRoute or removeRoute depending on the type of the message.
|
* routing data, invokes addRoute or removeRoute depending on the type of the message.
|
||||||
"""
|
"""
|
||||||
logging.info(f" [x] Received {message.body}, m={message.message_id}, p={message.properties}")
|
logging.info(
|
||||||
|
f" [x] Received {message.body}, m={message.message_id}, p={message.properties}"
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
logging.info(" [DBG] ******[%s] ROUTING_DATA %s >>> %s",
|
logging.info(" [DBG] ******[%s] ROUTING_DATA %s >>> %s",
|
||||||
delivery.getEnvelope().getDeliveryTag(),
|
delivery.getEnvelope().getDeliveryTag(),
|
||||||
delivery.getEnvelope().toString(), str(delivery.getBody()))
|
delivery.getEnvelope().toString(), str(delivery.getBody()))
|
||||||
"""
|
"""
|
||||||
logging.info(" [%s] ACK message now so that it is not being redelivered", message.delivery_tag)
|
logging.info(
|
||||||
|
" [%s] ACK message now so that it is not being redelivered",
|
||||||
|
message.delivery_tag,
|
||||||
|
)
|
||||||
await message.ack(multiple=False)
|
await message.ack(multiple=False)
|
||||||
cm_message: ServiceMessage = self.service_message_factory.from_bytes(message.body)
|
cm_message: ServiceMessage = self.service_message_factory.from_bytes(
|
||||||
|
message.body
|
||||||
|
)
|
||||||
# ignore the message if it comes from ourselves (even if from different instance)
|
# ignore the message if it comes from ourselves (even if from different instance)
|
||||||
logging.info(" [DBG] ******[%s] ROUTING_DATA, msg=%s, routeMap=[%s] sn=%s, replyTo=%s",
|
logging.info(
|
||||||
message.delivery_tag,
|
" [DBG] ******[%s] ROUTING_DATA, msg=%s, routeMap=[%s] sn=%s, replyTo=%s",
|
||||||
self.service_message_factory.to_string(cm_message),
|
message.delivery_tag,
|
||||||
self.amq_configuration.amq_adapter.route_mapping,
|
self.service_message_factory.to_string(cm_message),
|
||||||
self.amq_configuration.amq_adapter.service_name,
|
self.amq_configuration.amq_adapter.route_mapping,
|
||||||
cm_message.reply_to)
|
self.amq_configuration.amq_adapter.service_name,
|
||||||
if cm_message.is_valid() and not self.amq_configuration.amq_adapter.service_name == cm_message.reply_to:
|
cm_message.reply_to,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
cm_message.is_valid()
|
||||||
|
and not self.amq_configuration.amq_adapter.service_name
|
||||||
|
== cm_message.reply_to
|
||||||
|
):
|
||||||
if cm_message.message_type == ServiceMessageType.ROUTING_DATA:
|
if cm_message.message_type == ServiceMessageType.ROUTING_DATA:
|
||||||
self.add_routes(cm_message.message)
|
self.add_routes(cm_message.message)
|
||||||
if cm_message.message_type == ServiceMessageType.ROUTING_DATA_REMOVE:
|
if cm_message.message_type == ServiceMessageType.ROUTING_DATA_REMOVE:
|
||||||
@@ -111,72 +157,83 @@ class RouterProducer(RouterBase):
|
|||||||
|
|
||||||
async def request_routes_from_adapters(self):
|
async def request_routes_from_adapters(self):
|
||||||
"""
|
"""
|
||||||
* Send a broadcast message prompting all listening adapters to reply with own routing data.
|
* Send a broadcast message prompting all listening adapters to reply with own routing data.
|
||||||
"""
|
"""
|
||||||
logging.info(" [*] RouterProducer.requestRoutesFromAdapters, queue name: %s", CM_REQUEST_EXCHANGE)
|
logging.info(
|
||||||
|
" [*] RouterProducer.requestRoutesFromAdapters, queue name: %s",
|
||||||
|
CM_REQUEST_EXCHANGE,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
serviceMessage: ServiceMessage = self.service_message_factory.of(
|
serviceMessage: ServiceMessage = self.service_message_factory.of(
|
||||||
ServiceMessageType.ROUTING_DATA_REQUEST, "", ANY_RECIPIENT
|
ServiceMessageType.ROUTING_DATA_REQUEST, "", ANY_RECIPIENT
|
||||||
)
|
)
|
||||||
if not await self.publish_service_message(serviceMessage, self.cm_request_exchange):
|
if not await self.publish_service_message(
|
||||||
logging.warning(" [E] RouterProducer could not request route data: %s channel is not open.",
|
serviceMessage, self.cm_request_exchange
|
||||||
CM_REQUEST_EXCHANGE)
|
):
|
||||||
|
logging.warning(
|
||||||
|
" [E] RouterProducer could not request route data: %s channel is not open.",
|
||||||
|
CM_REQUEST_EXCHANGE,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as ioe:
|
except Exception as ioe:
|
||||||
logging_error("RouterProducer failed request routing data: %s", ioe)
|
logging_error("RouterProducer failed request routing data: %s", ioe)
|
||||||
|
|
||||||
async def publish_service_message(self,
|
async def publish_service_message(
|
||||||
message: ServiceMessage,
|
self, message: ServiceMessage, exchange: AbstractRobustExchange
|
||||||
exchange: AbstractRobustExchange) -> bool:
|
) -> bool:
|
||||||
|
"""
|
||||||
|
* Publishes a service message to the service queue and logs success log entry.
|
||||||
|
*
|
||||||
|
* :param message: Service Message to be sent
|
||||||
|
* :param exchange: target exchange name where to publish the message
|
||||||
|
* :return True if channel is open, False otherwise
|
||||||
|
* @throws IOException if something else goes wrong
|
||||||
"""
|
"""
|
||||||
* Publishes a service message to the service queue and logs success log entry.
|
|
||||||
*
|
|
||||||
* :param message: Service Message to be sent
|
|
||||||
* :param exchange: target exchange name where to publish the message
|
|
||||||
* :return True if channel is open, False otherwise
|
|
||||||
* @throws IOException if something else goes wrong
|
|
||||||
"""
|
|
||||||
if self.is_open(self.channel):
|
if self.is_open(self.channel):
|
||||||
binary_content: bytes = self.service_message_factory.serialize(message)
|
binary_content: bytes = self.service_message_factory.serialize(message)
|
||||||
pika_message: Message = Message(
|
pika_message: Message = Message(
|
||||||
body=binary_content,
|
body=binary_content,
|
||||||
content_encoding='utf-8',
|
content_encoding="utf-8",
|
||||||
delivery_mode=2,
|
delivery_mode=2,
|
||||||
content_type="application/octet-stream",
|
content_type="application/octet-stream",
|
||||||
headers=None,
|
headers=None,
|
||||||
priority=0,
|
priority=0,
|
||||||
correlation_id=None
|
correlation_id=None,
|
||||||
)
|
)
|
||||||
await exchange.publish(message=pika_message, routing_key="")
|
await exchange.publish(message=pika_message, routing_key="")
|
||||||
logging.info(" [x] Service Message Sent to %s, msg: %s", exchange.name, str(binary_content))
|
logging.info(
|
||||||
|
" [x] Service Message Sent to %s, msg: %s",
|
||||||
|
exchange.name,
|
||||||
|
str(binary_content),
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_reply_to_queue_name(self) -> str:
|
def get_reply_to_queue_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
* self should create a unique name for self process and also when there's several instances.
|
* self should create a unique name for self process and also when there's several instances.
|
||||||
* @return unique reply-to queue name.
|
* @return unique reply-to queue name.
|
||||||
"""
|
"""
|
||||||
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_REPLY_TO
|
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_REPLY_TO
|
||||||
|
|
||||||
def get_response_queue_name(self) -> str:
|
def get_response_queue_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
* self should create a unique name for self process and also when there's several instances.
|
* self should create a unique name for self process and also when there's several instances.
|
||||||
* @return unique reply-to queue name.
|
* @return unique reply-to queue name.
|
||||||
"""
|
"""
|
||||||
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_RESP_QUEUE
|
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_RESP_QUEUE
|
||||||
|
|
||||||
def get_consume_queue_name(self) -> str:
|
def get_consume_queue_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
* self should create a unique name for self process and also when there's several instances.
|
* self should create a unique name for self process and also when there's several instances.
|
||||||
* @return unique reply-to queue name.
|
* @return unique reply-to queue name.
|
||||||
"""
|
"""
|
||||||
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_AMQ_QUEUE
|
return self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_AMQ_QUEUE
|
||||||
|
|
||||||
def is_open(self, _channel: AbstractRobustChannel) -> bool:
|
def is_open(self, _channel: AbstractRobustChannel) -> bool:
|
||||||
"""
|
"""
|
||||||
* Test if the channel is usable (opened).
|
* Test if the channel is usable (opened).
|
||||||
* :param _channel: the channel
|
* :param _channel: the channel
|
||||||
* :return True if the channel is opened.
|
* :return True if the channel is opened.
|
||||||
"""
|
"""
|
||||||
return _channel is not None and not _channel.is_closed
|
return _channel is not None and not _channel.is_closed
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ class TestRouteDatabase(TestCase):
|
|||||||
|
|
||||||
def test_get_routes(self):
|
def test_get_routes(self):
|
||||||
_amq_factory = AMQRouteFactory()
|
_amq_factory = AMQRouteFactory()
|
||||||
_route: AMQRoute = _amq_factory.from_string("amq-clever-pybook::clever-pybook:clever.pybook.localhost.#:5:0")
|
_route: AMQRoute = _amq_factory.from_string(
|
||||||
|
"amq-clever-pybook::clever-pybook:clever.pybook.localhost.#:5:0"
|
||||||
|
)
|
||||||
_routes = self._database.get_routes()
|
_routes = self._database.get_routes()
|
||||||
self.assertIsNotNone(_routes)
|
self.assertIsNotNone(_routes)
|
||||||
self.assertEqual(3, len(_routes))
|
self.assertEqual(3, len(_routes))
|
||||||
@@ -76,8 +78,7 @@ class TestRouteDatabase(TestCase):
|
|||||||
|
|
||||||
def test_find_route(self):
|
def test_find_route(self):
|
||||||
message = DataMessageFactory.get_instance(111).create_request_message(
|
message = DataMessageFactory.get_instance(111).create_request_message(
|
||||||
"POST", "clever3-book.localhost",
|
"POST", "clever3-book.localhost", "/aa/bb?cc=d", {}, ""
|
||||||
"/aa/bb?cc=d", {}, ""
|
|
||||||
)
|
)
|
||||||
route = self._database.find_route(message.domain, message.path)
|
route = self._database.find_route(message.domain, message.path)
|
||||||
self.assertIsNotNone(route)
|
self.assertIsNotNone(route)
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class TestRouterBase(TestCase):
|
|||||||
self.assertEqual("aa.bb.cc", sanitize_as_rabbitmq_name("aa///bb////cc"))
|
self.assertEqual("aa.bb.cc", sanitize_as_rabbitmq_name("aa///bb////cc"))
|
||||||
|
|
||||||
def test_get_routing_key(self):
|
def test_get_routing_key(self):
|
||||||
message = self.factory.create_request_message("POST", "test.me.localhost", "/aa/bb?cc=d", {}, "")
|
message = self.factory.create_request_message(
|
||||||
|
"POST", "test.me.localhost", "/aa/bb?cc=d", {}, ""
|
||||||
|
)
|
||||||
rk = self.base.get_routing_key(message)
|
rk = self.base.get_routing_key(message)
|
||||||
self.assertEqual("test.me.localhost.aa.bb", rk)
|
self.assertEqual("test.me.localhost.aa.bb", rk)
|
||||||
|
|
||||||
@@ -80,8 +82,7 @@ class TestRouterBase(TestCase):
|
|||||||
|
|
||||||
def test_find_route_by_message(self):
|
def test_find_route_by_message(self):
|
||||||
message = DataMessageFactory.get_instance(111).create_request_message(
|
message = DataMessageFactory.get_instance(111).create_request_message(
|
||||||
"POST", "clever3-book.localhost",
|
"POST", "clever3-book.localhost", "/aa/bb?cc=d", {}, ""
|
||||||
"/aa/bb?cc=d", {}, ""
|
|
||||||
)
|
)
|
||||||
_route: AMQRoute = self.base.find_route_by_message(message)
|
_route: AMQRoute = self.base.find_route_by_message(message)
|
||||||
self.assertIsNotNone(_route)
|
self.assertIsNotNone(_route)
|
||||||
@@ -91,7 +92,7 @@ class TestRouterBase(TestCase):
|
|||||||
init_routes: Set[AMQRoute] = self.base.get_routes().copy()
|
init_routes: Set[AMQRoute] = self.base.get_routes().copy()
|
||||||
self.base.add_routes(ROUTE_6 + ",amq-dlq:DLX:DeadLetterQueue:#:0:0,:::::,:::")
|
self.base.add_routes(ROUTE_6 + ",amq-dlq:DLX:DeadLetterQueue:#:0:0,:::::,:::")
|
||||||
modified = self.base.get_routes()
|
modified = self.base.get_routes()
|
||||||
self.assertEqual(len(init_routes)+1, len(modified))
|
self.assertEqual(len(init_routes) + 1, len(modified))
|
||||||
wr = self.base.wrap_routes()
|
wr = self.base.wrap_routes()
|
||||||
self.assertFalse("DLX" in wr)
|
self.assertFalse("DLX" in wr)
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ pattern = re.compile(r"[^a-zA-Z0-9.#/\-]")
|
|||||||
|
|
||||||
def sanitize_as_rabbitmq_name(input_str: str) -> str:
|
def sanitize_as_rabbitmq_name(input_str: str) -> str:
|
||||||
matcher = pattern.search(input_str)
|
matcher = pattern.search(input_str)
|
||||||
token = input_str[:matcher.start()] if matcher else input_str
|
token = input_str[: matcher.start()] if matcher else input_str
|
||||||
return sanitize_as_rabbitmq_domain_name(token)
|
return sanitize_as_rabbitmq_domain_name(token)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_as_rabbitmq_domain_name(token: str) -> str:
|
def sanitize_as_rabbitmq_domain_name(token: str) -> str:
|
||||||
# Step 1: Replace non-alphanumeric characters (except periods) with a period
|
# Step 1: Replace non-alphanumeric characters (except periods) with a period
|
||||||
step1 = re.sub(r'[^A-Za-z0-9.#]', '.', token)
|
step1 = re.sub(r"[^A-Za-z0-9.#]", ".", token)
|
||||||
# Step 2: Replace multiple consecutive periods with a single period
|
# Step 2: Replace multiple consecutive periods with a single period
|
||||||
step2 = re.sub(r'\.+', '.', step1)
|
step2 = re.sub(r"\.+", ".", step1)
|
||||||
# Step 3: Remove trailing '.' character, if applicable
|
# Step 3: Remove trailing '.' character, if applicable
|
||||||
step_last_char = len(step2) - 1
|
step_last_char = len(step2) - 1
|
||||||
return step2[:-1] if step_last_char > 0 and step2[step_last_char] == '.' else step2
|
return step2[:-1] if step_last_char > 0 and step2[step_last_char] == "." else step2
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ from amqp.adapter.file_downloader import on_message
|
|||||||
from amqp.adapter.logging_utils import logging_error
|
from amqp.adapter.logging_utils import logging_error
|
||||||
from amqp.adapter.service_message_factory import ServiceMessageFactory
|
from amqp.adapter.service_message_factory import ServiceMessageFactory
|
||||||
from amqp.config.amq_configuration import AMQConfiguration
|
from amqp.config.amq_configuration import AMQConfiguration
|
||||||
from amqp.model.model import DataResponse, DataMessage, MultipartDataMessage, ScalingRequest, ScalingRequestAlert
|
from amqp.model.model import (
|
||||||
|
DataResponse,
|
||||||
|
DataMessage,
|
||||||
|
MultipartDataMessage,
|
||||||
|
ScalingRequest,
|
||||||
|
ScalingRequestAlert,
|
||||||
|
)
|
||||||
from amqp.model.service_message_type import ServiceMessageType
|
from amqp.model.service_message_type import ServiceMessageType
|
||||||
from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
|
from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
|
||||||
from amqp.router.router_base import ANY_RECIPIENT
|
from amqp.router.router_base import ANY_RECIPIENT
|
||||||
@@ -31,8 +37,10 @@ from amqp.adapter.serializer import map_as_string
|
|||||||
|
|
||||||
# Track the number of messages currently being processed
|
# Track the number of messages currently being processed
|
||||||
current_parallel_executions = 0
|
current_parallel_executions = 0
|
||||||
last_data_message_time = 0 # helps detect IDLE condition
|
last_data_message_time = 0 # helps detect IDLE condition
|
||||||
last_backpressure_event_time = 0 # helps prevent flooding the system with backpressure events
|
last_backpressure_event_time = (
|
||||||
|
0 # helps prevent flooding the system with backpressure events
|
||||||
|
)
|
||||||
last_backpressure_event = ScalingRequestAlert.IDLE
|
last_backpressure_event = ScalingRequestAlert.IDLE
|
||||||
|
|
||||||
parallel_workers = int(os.getenv("PARALLEL_WORKERS", default=0))
|
parallel_workers = int(os.getenv("PARALLEL_WORKERS", default=0))
|
||||||
@@ -45,8 +53,7 @@ executor = concurrent.futures.ThreadPoolExecutor(max_workers=parallel_workers +
|
|||||||
def collect_trace_info():
|
def collect_trace_info():
|
||||||
trace_map = {}
|
trace_map = {}
|
||||||
TraceContextTextMapPropagator().inject(
|
TraceContextTextMapPropagator().inject(
|
||||||
carrier=trace_map,
|
carrier=trace_map, context=trace.context_api.get_current()
|
||||||
context=trace.context_api.get_current()
|
|
||||||
)
|
)
|
||||||
return trace_map
|
return trace_map
|
||||||
|
|
||||||
@@ -60,15 +67,17 @@ def set_text(carrier, key, value):
|
|||||||
|
|
||||||
|
|
||||||
class DataMessageHandler:
|
class DataMessageHandler:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
service_adapter: CleverThisServiceAdapter,
|
self,
|
||||||
tracer: Tracer,
|
service_adapter: CleverThisServiceAdapter,
|
||||||
message_factory: DataMessageFactory,
|
tracer: Tracer,
|
||||||
service_message_factory: ServiceMessageFactory,
|
message_factory: DataMessageFactory,
|
||||||
reply_to_exchange: AbstractRobustExchange,
|
service_message_factory: ServiceMessageFactory,
|
||||||
rabbit_mq_client: RabbitMQClient,
|
reply_to_exchange: AbstractRobustExchange,
|
||||||
amq_configuration: AMQConfiguration,
|
rabbit_mq_client: RabbitMQClient,
|
||||||
loop):
|
amq_configuration: AMQConfiguration,
|
||||||
|
loop,
|
||||||
|
):
|
||||||
self.service_adapter: CleverThisServiceAdapter = service_adapter
|
self.service_adapter: CleverThisServiceAdapter = service_adapter
|
||||||
self.tracer = tracer
|
self.tracer = tracer
|
||||||
self.message_factory: DataMessageFactory = message_factory
|
self.message_factory: DataMessageFactory = message_factory
|
||||||
@@ -82,7 +91,9 @@ class DataMessageHandler:
|
|||||||
# Dictionary to store outstanding Futures
|
# Dictionary to store outstanding Futures
|
||||||
self.outstanding: Dict[str, asyncio.Future] = {}
|
self.outstanding: Dict[str, asyncio.Future] = {}
|
||||||
self.reply_to: str | None = None
|
self.reply_to: str | None = None
|
||||||
self.backpressure_monitor_window = amq_configuration.backpressure.time_window / 1000 # convert to seconds
|
self.backpressure_monitor_window = (
|
||||||
|
amq_configuration.backpressure.time_window / 1000
|
||||||
|
) # convert to seconds
|
||||||
|
|
||||||
global parallel_workers
|
global parallel_workers
|
||||||
_backpressure_treshold = amq_configuration.backpressure.threshold
|
_backpressure_treshold = amq_configuration.backpressure.threshold
|
||||||
@@ -104,7 +115,10 @@ class DataMessageHandler:
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
event: ScalingRequestAlert = ScalingRequestAlert.UPDATE
|
event: ScalingRequestAlert = ScalingRequestAlert.UPDATE
|
||||||
if now - last_backpressure_event_time >= self.backpressure_monitor_window:
|
if now - last_backpressure_event_time >= self.backpressure_monitor_window:
|
||||||
if current_parallel_executions == 0 and now - last_data_message_time > self.backpressure_monitor_window:
|
if (
|
||||||
|
current_parallel_executions == 0
|
||||||
|
and now - last_data_message_time > self.backpressure_monitor_window
|
||||||
|
):
|
||||||
event = ScalingRequestAlert.IDLE
|
event = ScalingRequestAlert.IDLE
|
||||||
elif current_parallel_executions > parallel_workers - 2:
|
elif current_parallel_executions > parallel_workers - 2:
|
||||||
event = ScalingRequestAlert.OVERLOAD
|
event = ScalingRequestAlert.OVERLOAD
|
||||||
@@ -112,29 +126,36 @@ class DataMessageHandler:
|
|||||||
last_backpressure_event_time = now
|
last_backpressure_event_time = now
|
||||||
last_backpressure_event = event
|
last_backpressure_event = event
|
||||||
last_backpressure_event_time = now
|
last_backpressure_event_time = now
|
||||||
logging.info(f"Backpressure event: {event}, threads: {current_parallel_executions} of {parallel_workers}")
|
logging.info(
|
||||||
|
f"Backpressure event: {event}, threads: {current_parallel_executions} of {parallel_workers}"
|
||||||
|
)
|
||||||
|
|
||||||
scaling_request: ScalingRequest = ScalingRequest(
|
scaling_request: ScalingRequest = ScalingRequest(
|
||||||
swarm_service_id, swarm_task_id,
|
swarm_service_id,
|
||||||
parallel_workers, parallel_workers - current_parallel_executions,
|
swarm_task_id,
|
||||||
event
|
parallel_workers,
|
||||||
|
parallel_workers - current_parallel_executions,
|
||||||
|
event,
|
||||||
)
|
)
|
||||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
||||||
scale_up_request = self.service_message_factory.of(
|
scale_up_request = self.service_message_factory.of(
|
||||||
ServiceMessageType.BACKPRESSURE,
|
ServiceMessageType.BACKPRESSURE,
|
||||||
scaling_request.to_json(),
|
scaling_request.to_json(),
|
||||||
self.reply_to if self.reply_to else ANY_RECIPIENT
|
self.reply_to if self.reply_to else ANY_RECIPIENT,
|
||||||
)
|
)
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self.rabbit_mq_client.router.publish_service_message(
|
self.rabbit_mq_client.router.publish_service_message(
|
||||||
scale_up_request, exchange=self.rabbit_mq_client.router.cm_response_exchange
|
scale_up_request,
|
||||||
|
exchange=self.rabbit_mq_client.router.cm_response_exchange,
|
||||||
),
|
),
|
||||||
loop=self.loop
|
loop=self.loop,
|
||||||
)
|
)
|
||||||
await asyncio.sleep(self.backpressure_monitor_window) # sleep in seconds
|
await asyncio.sleep(self.backpressure_monitor_window) # sleep in seconds
|
||||||
|
|
||||||
async def inbound_data_message_callback(self, message: AbstractIncomingMessage):
|
async def inbound_data_message_callback(self, message: AbstractIncomingMessage):
|
||||||
threading.Thread(target=lambda: asyncio.run(self.run_callback_thread(message))).start()
|
threading.Thread(
|
||||||
|
target=lambda: asyncio.run(self.run_callback_thread(message))
|
||||||
|
).start()
|
||||||
|
|
||||||
async def run_callback_thread(self, message: AbstractIncomingMessage):
|
async def run_callback_thread(self, message: AbstractIncomingMessage):
|
||||||
"""
|
"""
|
||||||
@@ -151,12 +172,16 @@ class DataMessageHandler:
|
|||||||
if amq_message.magic == DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST:
|
if amq_message.magic == DataMessageFactory.MAGIC_BYTE_HTTP_REQUEST:
|
||||||
await self.run_http_request_magic(message, amq_message, self.loop)
|
await self.run_http_request_magic(message, amq_message, self.loop)
|
||||||
else:
|
else:
|
||||||
logging_error("Unknown MAGIC value: [%s], don't know how to interpret the message",
|
logging_error(
|
||||||
amq_message.magic)
|
"Unknown MAGIC value: [%s], don't know how to interpret the message",
|
||||||
|
amq_message.magic,
|
||||||
|
)
|
||||||
asyncio.run_coroutine_threadsafe(message.nack(requeue=False), self.loop)
|
asyncio.run_coroutine_threadsafe(message.nack(requeue=False), self.loop)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
logging_error("######[%s] No valid AMQ Message present.", message.delivery_tag)
|
logging_error(
|
||||||
|
"######[%s] No valid AMQ Message present.", message.delivery_tag
|
||||||
|
)
|
||||||
self.record_duration(start, message.delivery_tag)
|
self.record_duration(start, message.delivery_tag)
|
||||||
asyncio.run_coroutine_threadsafe(message.ack(multiple=False), self.loop)
|
asyncio.run_coroutine_threadsafe(message.ack(multiple=False), self.loop)
|
||||||
|
|
||||||
@@ -164,30 +189,35 @@ class DataMessageHandler:
|
|||||||
logging.warning("Warning: Capacity close to depleted!")
|
logging.warning("Warning: Capacity close to depleted!")
|
||||||
# Send a scaling request to the Management service
|
# Send a scaling request to the Management service
|
||||||
scaling_request: ScalingRequest = ScalingRequest(
|
scaling_request: ScalingRequest = ScalingRequest(
|
||||||
swarm_service_id, swarm_task_id,
|
swarm_service_id,
|
||||||
parallel_workers, parallel_workers - current_parallel_executions,
|
swarm_task_id,
|
||||||
ScalingRequestAlert.OVERLOAD
|
parallel_workers,
|
||||||
|
parallel_workers - current_parallel_executions,
|
||||||
|
ScalingRequestAlert.OVERLOAD,
|
||||||
)
|
)
|
||||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
||||||
scale_up_request = self.service_message_factory.of(
|
scale_up_request = self.service_message_factory.of(
|
||||||
ServiceMessageType.BACKPRESSURE,
|
ServiceMessageType.BACKPRESSURE,
|
||||||
scaling_request.to_json(),
|
scaling_request.to_json(),
|
||||||
self.reply_to if self.reply_to else ANY_RECIPIENT
|
self.reply_to if self.reply_to else ANY_RECIPIENT,
|
||||||
)
|
)
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self.rabbit_mq_client.router.publish_service_message(
|
self.rabbit_mq_client.router.publish_service_message(
|
||||||
scale_up_request, exchange=self.rabbit_mq_client.router.cm_response_exchange
|
scale_up_request,
|
||||||
|
exchange=self.rabbit_mq_client.router.cm_response_exchange,
|
||||||
),
|
),
|
||||||
loop=_loop
|
loop=_loop,
|
||||||
)
|
)
|
||||||
# update / reset time-window so that the OVERLOAD is not sent too often
|
# update / reset time-window so that the OVERLOAD is not sent too often
|
||||||
global last_data_message_time
|
global last_data_message_time
|
||||||
last_datadeliveryTag_message_time = time.time()
|
last_datadeliveryTag_message_time = time.time()
|
||||||
|
|
||||||
async def run_http_request_magic(self,
|
async def run_http_request_magic(
|
||||||
message: AbstractIncomingMessage,
|
self,
|
||||||
amq_message: DataMessage,
|
message: AbstractIncomingMessage,
|
||||||
_loop: AbstractEventLoop) -> None:
|
amq_message: DataMessage,
|
||||||
|
_loop: AbstractEventLoop,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Synchronous version of on_message method. This is a wrapper around the async on_message method.
|
Synchronous version of on_message method. This is a wrapper around the async on_message method.
|
||||||
It also counts parallel executions and handles backpressure.
|
It also counts parallel executions and handles backpressure.
|
||||||
@@ -200,7 +230,9 @@ class DataMessageHandler:
|
|||||||
|
|
||||||
# Increment the counter for parallel executions
|
# Increment the counter for parallel executions
|
||||||
current_parallel_executions += 1
|
current_parallel_executions += 1
|
||||||
logging.info(f"PARALLEL: Current Capacity [{current_parallel_executions}] of [{parallel_workers}]")
|
logging.info(
|
||||||
|
f"PARALLEL: Current Capacity [{current_parallel_executions}] of [{parallel_workers}]"
|
||||||
|
)
|
||||||
if message.reply_to:
|
if message.reply_to:
|
||||||
self.reply_to = message.reply_to
|
self.reply_to = message.reply_to
|
||||||
if current_parallel_executions > parallel_workers - 2:
|
if current_parallel_executions > parallel_workers - 2:
|
||||||
@@ -211,20 +243,31 @@ class DataMessageHandler:
|
|||||||
response: DataResponse = await self.service_adapter.on_message(a_message)
|
response: DataResponse = await self.service_adapter.on_message(a_message)
|
||||||
print(f"Result in thread: {response}")
|
print(f"Result in thread: {response}")
|
||||||
try:
|
try:
|
||||||
logging.info(" [*] ######[%s] AMQ Message Reply.1, To=%s, msgId=%s",
|
logging.info(
|
||||||
message.delivery_tag, message.reply_to, response.id)
|
" [*] ######[%s] AMQ Message Reply.1, To=%s, msgId=%s",
|
||||||
|
message.delivery_tag,
|
||||||
|
message.reply_to,
|
||||||
|
response.id,
|
||||||
|
)
|
||||||
await self.send_reply(message.reply_to, response)
|
await self.send_reply(message.reply_to, response)
|
||||||
logging.info(" [*] ######[%s] AMQ Message Reply.2, To=%s",
|
logging.info(
|
||||||
message.delivery_tag, message.reply_to)
|
" [*] ######[%s] AMQ Message Reply.2, To=%s",
|
||||||
|
message.delivery_tag,
|
||||||
|
message.reply_to,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(f"Processing message: {e}")
|
logging_error(f"Processing message: {e}")
|
||||||
asyncio.run_coroutine_threadsafe(message.nack(requeue=False), loop=_loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
message.nack(requeue=False), loop=_loop
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(amq_message, MultipartDataMessage):
|
if isinstance(amq_message, MultipartDataMessage):
|
||||||
# remove the downloaded files from the output_dir
|
# remove the downloaded files from the output_dir
|
||||||
for file_id, filename in amq_message.extra_data.items():
|
for file_id, filename in amq_message.extra_data.items():
|
||||||
try:
|
try:
|
||||||
logging.info(f" [*][{message.delivery_tag}] Deleting file: {file_id} in {filename}")
|
logging.info(
|
||||||
|
f" [*][{message.delivery_tag}] Deleting file: {file_id} in {filename}"
|
||||||
|
)
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logging_error(f"Deleting file {filename}: {e}")
|
logging_error(f"Deleting file {filename}: {e}")
|
||||||
@@ -234,34 +277,50 @@ class DataMessageHandler:
|
|||||||
logging_error(f"Request handler: {e}")
|
logging_error(f"Request handler: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def reconstitute_data_message(
|
||||||
async def reconstitute_data_message(self, message: AbstractIncomingMessage) -> DataMessage | None:
|
self, message: AbstractIncomingMessage
|
||||||
|
) -> DataMessage | None:
|
||||||
amq_message: DataMessage | None = None
|
amq_message: DataMessage | None = None
|
||||||
delivery_tag = message.delivery_tag
|
delivery_tag = message.delivery_tag
|
||||||
addressing: int = message.headers.get('clevermicro.addressing', 0)
|
addressing: int = message.headers.get("clevermicro.addressing", 0)
|
||||||
if addressing == 0:
|
if addressing == 0:
|
||||||
amq_message = DataMessageFactory.from_bytes(message.body)
|
amq_message = DataMessageFactory.from_bytes(message.body)
|
||||||
else:
|
else:
|
||||||
amq_message = await DataMessageFactory.from_stream(message.body, rabbitmq_url=self.rabbit_mq_url, loop=self.loop)
|
amq_message = await DataMessageFactory.from_stream(
|
||||||
logging.info(f" [DBG] ######[{delivery_tag}] AMQ Message from stream: {amq_message}")
|
message.body, rabbitmq_url=self.rabbit_mq_url, loop=self.loop
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
f" [DBG] ######[{delivery_tag}] AMQ Message from stream: {amq_message}"
|
||||||
|
)
|
||||||
if amq_message is not None:
|
if amq_message is not None:
|
||||||
extra_data = await on_message(
|
extra_data = await on_message(
|
||||||
message=message,
|
message=message,
|
||||||
json_data=amq_message.body(),
|
json_data=amq_message.body(),
|
||||||
rabbitmq_url=self.rabbit_mq_url,
|
rabbitmq_url=self.rabbit_mq_url,
|
||||||
loop=self.loop,
|
loop=self.loop,
|
||||||
output_dir=self.output_dir)
|
output_dir=self.output_dir,
|
||||||
logging.info(f" [x] ######[{delivery_tag}] Multipart download result: {extra_data}")
|
)
|
||||||
|
logging.info(
|
||||||
|
f" [x] ######[{delivery_tag}] Multipart download result: {extra_data}"
|
||||||
|
)
|
||||||
amq_message = MultipartDataMessage(amq_message, extra_data)
|
amq_message = MultipartDataMessage(amq_message, extra_data)
|
||||||
if amq_message:
|
if amq_message:
|
||||||
logging.info(" [*] ######[%s] AMQ Message received, ctag=%s, msgId=%s, %s:%s, traceInfo=%s",
|
logging.info(
|
||||||
message.delivery_tag, message.consumer_tag, amq_message.id,
|
" [*] ######[%s] AMQ Message received, ctag=%s, msgId=%s, %s:%s, traceInfo=%s",
|
||||||
amq_message.method, amq_message.path, map_as_string(amq_message.trace_info))
|
message.delivery_tag,
|
||||||
|
message.consumer_tag,
|
||||||
|
amq_message.id,
|
||||||
|
amq_message.method,
|
||||||
|
amq_message.path,
|
||||||
|
map_as_string(amq_message.trace_info),
|
||||||
|
)
|
||||||
return amq_message
|
return amq_message
|
||||||
|
|
||||||
def ensure_trace_info(self, amq_message: DataMessage):
|
def ensure_trace_info(self, amq_message: DataMessage):
|
||||||
propagator = TraceContextTextMapPropagator()
|
propagator = TraceContextTextMapPropagator()
|
||||||
context = propagator.extract(carrier=amq_message.trace_info, context=trace.context_api.get_current())
|
context = propagator.extract(
|
||||||
|
carrier=amq_message.trace_info, context=trace.context_api.get_current()
|
||||||
|
)
|
||||||
self.tracer.start_span(name="AMQ-Adapter-Consumer", context=context)
|
self.tracer.start_span(name="AMQ-Adapter-Consumer", context=context)
|
||||||
logging.info(" [*] CTX=%s", context)
|
logging.info(" [*] CTX=%s", context)
|
||||||
# current_span = self.tracer.start_span("AMQ-Adapter.Processing.Request", parent=parent_span)
|
# current_span = self.tracer.start_span("AMQ-Adapter.Processing.Request", parent=parent_span)
|
||||||
@@ -274,7 +333,11 @@ class DataMessageHandler:
|
|||||||
def record_duration(self, start, delivery_tag):
|
def record_duration(self, start, delivery_tag):
|
||||||
global last_data_message_time
|
global last_data_message_time
|
||||||
last_data_message_time = time.time()
|
last_data_message_time = time.time()
|
||||||
logging.info(" [x] ######[%s] Done, single ACK: %sms.", delivery_tag, last_data_message_time - start)
|
logging.info(
|
||||||
|
" [x] ######[%s] Done, single ACK: %sms.",
|
||||||
|
delivery_tag,
|
||||||
|
last_data_message_time - start,
|
||||||
|
)
|
||||||
|
|
||||||
async def send_reply(self, reply_to: str, response: DataResponse):
|
async def send_reply(self, reply_to: str, response: DataResponse):
|
||||||
"""
|
"""
|
||||||
@@ -288,15 +351,27 @@ class DataMessageHandler:
|
|||||||
pika_message: Message = Message(
|
pika_message: Message = Message(
|
||||||
body=DataResponseFactory.serialize(response),
|
body=DataResponseFactory.serialize(response),
|
||||||
correlation_id=response.id,
|
correlation_id=response.id,
|
||||||
content_type=response.content_type[0]
|
content_type=(
|
||||||
if isinstance(response.content_type, list) else response.content_type
|
response.content_type[0]
|
||||||
|
if isinstance(response.content_type, list)
|
||||||
|
else response.content_type
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [*] ###### Sending Reply.1 To=%s, msgId=%s", reply_to, response.id
|
||||||
|
)
|
||||||
|
_res = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.reply_to_exchange.publish(
|
||||||
|
message=pika_message, routing_key=reply_to
|
||||||
|
),
|
||||||
|
self.loop,
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
" [*] ###### DONE EXCH PUB(%s) Sending Reply.2 To=%s, res=%s",
|
||||||
|
self.reply_to_exchange.name,
|
||||||
|
reply_to,
|
||||||
|
_res,
|
||||||
)
|
)
|
||||||
logging.info(" [*] ###### Sending Reply.1 To=%s, msgId=%s", reply_to, response.id)
|
|
||||||
_res = asyncio.run_coroutine_threadsafe(self.reply_to_exchange.publish(
|
|
||||||
message=pika_message,
|
|
||||||
routing_key=reply_to
|
|
||||||
), self.loop)
|
|
||||||
logging.info(" [*] ###### DONE EXCH PUB(%s) Sending Reply.2 To=%s, res=%s", self.reply_to_exchange.name, reply_to, _res)
|
|
||||||
|
|
||||||
async def reply_received_callback(self, message: AbstractIncomingMessage):
|
async def reply_received_callback(self, message: AbstractIncomingMessage):
|
||||||
"""
|
"""
|
||||||
@@ -312,7 +387,9 @@ class DataMessageHandler:
|
|||||||
logging.info(debug)
|
logging.info(debug)
|
||||||
# TODO - figure out implementation of this part, as this is sending reply back either to a service,
|
# TODO - figure out implementation of this part, as this is sending reply back either to a service,
|
||||||
# or to the user
|
# or to the user
|
||||||
logging.info(f"OUTSTANDING RECV: {self.outstanding}, size={len(self.outstanding)}")
|
logging.info(
|
||||||
|
f"OUTSTANDING RECV: {self.outstanding}, size={len(self.outstanding)}"
|
||||||
|
)
|
||||||
|
|
||||||
future: Future = self.outstanding.pop(reply_id, None)
|
future: Future = self.outstanding.pop(reply_id, None)
|
||||||
# if reply is None:
|
# if reply is None:
|
||||||
|
|||||||
+16
-15
@@ -19,14 +19,15 @@ from amqp.service.tracing import initialize_trace
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
logging.info(f"CWD = {os.getcwd()}")
|
logging.info(f"CWD = {os.getcwd()}")
|
||||||
logging_conf_path = os.path.join(os.getcwd(), 'amqp', 'service', 'logging.conf')
|
logging_conf_path = os.path.join(os.getcwd(), "amqp", "service", "logging.conf")
|
||||||
if os.path.exists(logging_conf_path):
|
if os.path.exists(logging_conf_path):
|
||||||
logging.config.fileConfig(logging_conf_path)
|
logging.config.fileConfig(logging_conf_path)
|
||||||
else:
|
else:
|
||||||
logging_conf_path = os.path.join(os.getcwd(), 'logging.conf')
|
logging_conf_path = os.path.join(os.getcwd(), "logging.conf")
|
||||||
if os.path.exists(logging_conf_path):
|
if os.path.exists(logging_conf_path):
|
||||||
logging.config.fileConfig(logging_conf_path)
|
logging.config.fileConfig(logging_conf_path)
|
||||||
|
|
||||||
|
|
||||||
class AMQService:
|
class AMQService:
|
||||||
CLEVER_AMQP_CLIENT_VERSION = os.getenv("CLEVER_AMQP_CLIENT_VERSION")
|
CLEVER_AMQP_CLIENT_VERSION = os.getenv("CLEVER_AMQP_CLIENT_VERSION")
|
||||||
MAX_WAIT = 300 # defaults to 5 minutes (300 seconds)
|
MAX_WAIT = 300 # defaults to 5 minutes (300 seconds)
|
||||||
@@ -38,7 +39,9 @@ class AMQService:
|
|||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
self.amq_configuration = amq_configuration
|
self.amq_configuration = amq_configuration
|
||||||
self.data_message_factory = DataMessageFactory.get_instance(self.amq_configuration.amq_adapter.generator_id)
|
self.data_message_factory = DataMessageFactory.get_instance(
|
||||||
|
self.amq_configuration.amq_adapter.generator_id
|
||||||
|
)
|
||||||
self.rabbit_mq_client = RabbitMQClient(self.amq_configuration)
|
self.rabbit_mq_client = RabbitMQClient(self.amq_configuration)
|
||||||
self.tracer = initialize_trace(name=amq_configuration.amq_adapter.service_name)
|
self.tracer = initialize_trace(name=amq_configuration.amq_adapter.service_name)
|
||||||
self.amq_data_message_handler = None
|
self.amq_data_message_handler = None
|
||||||
@@ -54,7 +57,9 @@ class AMQService:
|
|||||||
self.loop.run_forever()
|
self.loop.run_forever()
|
||||||
|
|
||||||
async def init(self, loop, service_adapter):
|
async def init(self, loop, service_adapter):
|
||||||
_reply_to_exchange: AbstractRobustExchange = await self.rabbit_mq_client.init(loop)
|
_reply_to_exchange: AbstractRobustExchange = await self.rabbit_mq_client.init(
|
||||||
|
loop
|
||||||
|
)
|
||||||
self.amq_data_message_handler = DataMessageHandler(
|
self.amq_data_message_handler = DataMessageHandler(
|
||||||
service_adapter,
|
service_adapter,
|
||||||
self.tracer,
|
self.tracer,
|
||||||
@@ -63,7 +68,7 @@ class AMQService:
|
|||||||
_reply_to_exchange,
|
_reply_to_exchange,
|
||||||
self.rabbit_mq_client,
|
self.rabbit_mq_client,
|
||||||
self.amq_configuration,
|
self.amq_configuration,
|
||||||
loop=loop
|
loop=loop,
|
||||||
)
|
)
|
||||||
await self.register_routes()
|
await self.register_routes()
|
||||||
await self.rabbit_mq_client.request_routes()
|
await self.rabbit_mq_client.request_routes()
|
||||||
@@ -85,7 +90,8 @@ class AMQService:
|
|||||||
if AMQRouteFactory.validate(route_mapping):
|
if AMQRouteFactory.validate(route_mapping):
|
||||||
try:
|
try:
|
||||||
await self.rabbit_mq_client.register_inbound_routes(
|
await self.rabbit_mq_client.register_inbound_routes(
|
||||||
route_mapping, self.amq_data_message_handler.inbound_data_message_callback
|
route_mapping,
|
||||||
|
self.amq_data_message_handler.inbound_data_message_callback,
|
||||||
)
|
)
|
||||||
return await self.rabbit_mq_client.register_data_reply_callback(
|
return await self.rabbit_mq_client.register_data_reply_callback(
|
||||||
self.amq_data_message_handler.reply_received_callback
|
self.amq_data_message_handler.reply_received_callback
|
||||||
@@ -93,7 +99,10 @@ class AMQService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(e)
|
raise RuntimeError(e)
|
||||||
elif route_mapping:
|
elif route_mapping:
|
||||||
logging.info(" [W] route [%s] failed validation, no routes were registered.", route_mapping)
|
logging.info(
|
||||||
|
" [W] route [%s] failed validation, no routes were registered.",
|
||||||
|
route_mapping,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def send_message(self, message: DataMessage, future: Future):
|
def send_message(self, message: DataMessage, future: Future):
|
||||||
@@ -101,11 +110,3 @@ class AMQService:
|
|||||||
|
|
||||||
def remove_outstanding_future(self, message_id: str):
|
def remove_outstanding_future(self, message_id: str):
|
||||||
self.amq_data_message_handler.outstanding.pop(message_id, None)
|
self.amq_data_message_handler.outstanding.pop(message_id, None)
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
# amq_configuration: AMQConfiguration = AMQConfiguration('../config/application.properties.local')
|
|
||||||
# amq_service: AMQService = AMQService(amq_configuration, CleverSwarmAmqpAdapter(amq_configuration))
|
|
||||||
# amq_service.rabbit_mq_client.channel.start_consuming()
|
|
||||||
# amq_service.consumer_thread.cancel()
|
|
||||||
# amq_service.rabbit_mq_client.connection.close()
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
def delete_queues_with_prefix_http(host='localhost', port=15672, username='springuser', password='TheCleverWho', prefix='cm-'):
|
def delete_queues_with_prefix_http(
|
||||||
|
host="localhost",
|
||||||
|
port=15672,
|
||||||
|
username="springuser",
|
||||||
|
password="TheCleverWho",
|
||||||
|
prefix="cm-",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
CLI utility. Delete queues using RabbitMQ HTTP API.
|
CLI utility. Delete queues using RabbitMQ HTTP API.
|
||||||
"""
|
"""
|
||||||
@@ -11,7 +17,7 @@ def delete_queues_with_prefix_http(host='localhost', port=15672, username='sprin
|
|||||||
response = requests.get(base_url, auth=(username, password))
|
response = requests.get(base_url, auth=(username, password))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
queues = [q['name'] for q in response.json() if q['name'].startswith(prefix)]
|
queues = [q["name"] for q in response.json() if q["name"].startswith(prefix)]
|
||||||
|
|
||||||
if not queues:
|
if not queues:
|
||||||
print(f"No queues found with prefix '{prefix}'")
|
print(f"No queues found with prefix '{prefix}'")
|
||||||
@@ -38,5 +44,5 @@ def delete_queues_with_prefix_http(host='localhost', port=15672, username='sprin
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
delete_queues_with_prefix_http()
|
delete_queues_with_prefix_http()
|
||||||
delete_queues_with_prefix_http(prefix='amq.gen--')
|
delete_queues_with_prefix_http(prefix="amq.gen--")
|
||||||
delete_queues_with_prefix_http(prefix='amq_')
|
delete_queues_with_prefix_http(prefix="amq_")
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def initialize_trace(app = None, name = None) -> Tracer:
|
def initialize_trace(app=None, name=None) -> Tracer:
|
||||||
# Resource can be required for some backends, e.g. Jaeger or Prometheus
|
# Resource can be required for some backends, e.g. Jaeger or Prometheus
|
||||||
# If resource wouldn't be set - traces wouldn't appear in Jaeger.
|
# If resource wouldn't be set - traces wouldn't appear in Jaeger.
|
||||||
# The 'name' value is the name shown in Jaeger.
|
# The 'name' value is the name shown in Jaeger.
|
||||||
resource = Resource(attributes={
|
resource = Resource(
|
||||||
SERVICE_NAME: name if name else os.environ.get('APPLICATION_NAME', 'Python App')
|
attributes={
|
||||||
})
|
SERVICE_NAME: (
|
||||||
|
name if name else os.environ.get("APPLICATION_NAME", "Python App")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
#
|
#
|
||||||
# 1. Tracing (Jaeger) configuration
|
# 1. Tracing (Jaeger) configuration
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
@@ -82,7 +86,6 @@ def initialize_trace(app = None, name = None) -> Tracer:
|
|||||||
# of that parent span, indicating visually the progression of requests via different services
|
# of that parent span, indicating visually the progression of requests via different services
|
||||||
# for as long as the current span is provided in the traceparent header to next service.
|
# for as long as the current span is provided in the traceparent header to next service.
|
||||||
|
|
||||||
|
|
||||||
# 2. Metrics (Prometheus) configuration
|
# 2. Metrics (Prometheus) configuration
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
#
|
#
|
||||||
@@ -97,4 +100,3 @@ def initialize_trace(app = None, name = None) -> Tracer:
|
|||||||
metrics.set_meter_provider(provider)
|
metrics.set_meter_provider(provider)
|
||||||
|
|
||||||
return trace.get_tracer(name)
|
return trace.get_tracer(name)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user