Compare commits

..

5 Commits

Author SHA1 Message Date
hurui200320 c6d7e9c003 test
Unit test coverage / pytest (push) Has been cancelled
Unit test coverage / pytest (pull_request) Has been cancelled
CI for pypl publish / publish-lib (push) Failing after 1m7s
2025-04-30 11:01:37 +08:00
hurui200320 7e7d4e5b37 Temporarily upload to forgejo 2025-04-30 11:01:37 +08:00
hurui200320 d714399cfc ci(forgejo): move pipeline to forgejo
ISSUES CLOSED: clevermicro/amq-adapter-python#8
2025-04-30 11:01:37 +08:00
stanislav.hejny 0f8bec742d Merge pull request '#26 - fix the boolean config value conversion, organize all config values to single class' (#27) from organize-config-values into develop
Reviewed-on: #27
2025-04-29 12:11:49 +00:00
Stanislav Hejny d1ba4db460 #26 - fix the boolean config value conversion, organize all config values to single class 2025-04-29 13:05:56 +01:00
7 changed files with 60 additions and 68 deletions
+18 -18
View File
@@ -1,23 +1,17 @@
import asyncio import asyncio
import json import json
import logging
import os
import time import time
from asyncio import AbstractEventLoop from asyncio import AbstractEventLoop
from threading import Thread from threading import Thread
from aio_pika import Message from aio_pika import Message
from aio_pika.abc import AbstractRobustChannel, ExchangeType from aio_pika.abc import AbstractRobustChannel
from amqp.adapter.logging_utils import logging_info, logging_warning from amqp.adapter.logging_utils import logging_info, logging_warning
from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import ScalingRequestAlert from amqp.model.model import ScalingRequestAlert
from amqp.router.utils import await_future, await_result from amqp.router.utils import await_future, await_result
# Duration in seconds between backpressure reports
REPORT_WINDOW_SEC = 10
# Number of reports without activity to trigger idle event
NUM_REPORTS_FOR_IDLE = 3
class ScaleRequestV1: class ScaleRequestV1:
@@ -61,15 +55,20 @@ class BackpressureHandler:
last_backpressure_event_time = 0 last_backpressure_event_time = 0
last_backpressure_event = ScalingRequestAlert.UPDATE last_backpressure_event = ScalingRequestAlert.UPDATE
parallel_workers = int(os.getenv("PARALLEL_WORKERS", default=0)) def __init__(
swarm_service_id = os.getenv("SWARM_SERVICE_ID", default="clever-amqp-service") self,
swarm_task_id = os.getenv("SWARM_TASK_ID", default="python-adapter") channel: AbstractRobustChannel,
loop: AbstractEventLoop,
def __init__(self, channel: AbstractRobustChannel, loop: AbstractEventLoop): config: AMQConfiguration,
):
self.lock = None self.lock = None
self.channel = channel self.channel = channel
self.loop = loop self.loop = loop
self.config = config
self.exchange = None self.exchange = None
self.swarm_service_id = self.config.amq_adapter.swarm_service_id
self.swarm_task_id = self.config.amq_adapter.swarm_task_id
self.parallel_workers = self.config.backpressure.threshold_threads
def increase_parallel_executions(self): def increase_parallel_executions(self):
"""Increase the number of parallel executions""" """Increase the number of parallel executions"""
@@ -120,7 +119,8 @@ class BackpressureHandler:
self.last_backpressure_event = ScalingRequestAlert.OVERLOAD self.last_backpressure_event = ScalingRequestAlert.OVERLOAD
def backpressure_monitor_loop(self): def backpressure_monitor_loop(self):
global REPORT_WINDOW_SEC _time_window_sec = self.config.backpressure.time_window
_idle_duration_millis = 1000 * self.config.backpressure.idle_duration
_loop = asyncio.new_event_loop() _loop = asyncio.new_event_loop()
async def _backpressure_monitor(): async def _backpressure_monitor():
@@ -143,7 +143,7 @@ class BackpressureHandler:
if ( if (
self.current_parallel_executions >= self.parallel_workers self.current_parallel_executions >= self.parallel_workers
and time.time() - self.last_backpressure_event_time and time.time() - self.last_backpressure_event_time
> REPORT_WINDOW_SEC > _time_window_sec
and self.last_backpressure_event != ScalingRequestAlert.OVERLOAD and self.last_backpressure_event != ScalingRequestAlert.OVERLOAD
): ):
# Trigger the overload event # Trigger the overload event
@@ -153,7 +153,7 @@ class BackpressureHandler:
elif ( elif (
self.current_parallel_executions == 0 self.current_parallel_executions == 0
and time.time() - self.last_data_message_time and time.time() - self.last_data_message_time
> NUM_REPORTS_FOR_IDLE * REPORT_WINDOW_SEC >= _idle_duration_millis
and self.last_backpressure_event != ScalingRequestAlert.IDLE and self.last_backpressure_event != ScalingRequestAlert.IDLE
): ):
# Trigger the idle event # Trigger the idle event
@@ -165,7 +165,7 @@ class BackpressureHandler:
if ( if (
self.current_parallel_executions > 0 self.current_parallel_executions > 0
and time.time() - self.last_backpressure_event_time and time.time() - self.last_backpressure_event_time
> REPORT_WINDOW_SEC > _time_window_sec
): ):
# Trigger the update event # Trigger the update event
_scaling_request: ScaleRequestV1 = ScaleRequestV1( _scaling_request: ScaleRequestV1 = ScaleRequestV1(
@@ -179,7 +179,7 @@ class BackpressureHandler:
await self.publish_backpressure_request(_scaling_request) await self.publish_backpressure_request(_scaling_request)
self.last_backpressure_event_time = time.time() self.last_backpressure_event_time = time.time()
self.last_backpressure_event = ScalingRequestAlert.UPDATE self.last_backpressure_event = ScalingRequestAlert.UPDATE
await asyncio.sleep(REPORT_WINDOW_SEC) await asyncio.sleep(_time_window_sec)
_loop.run_until_complete(_backpressure_monitor()) _loop.run_until_complete(_backpressure_monitor())
+3 -18
View File
@@ -6,7 +6,6 @@ Methods here will invoke the actual CleverThis Service code, which are passed as
import asyncio import asyncio
import inspect import inspect
import json import json
import logging
from asyncio import AbstractEventLoop from asyncio import AbstractEventLoop
from typing import Dict, List, Callable, Coroutine, Any from typing import Dict, List, Callable, Coroutine, Any
@@ -141,23 +140,9 @@ 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
try: self.require_authenticated_user = (
self.require_authenticated_user = ( self.amq_configuration.amq_adapter.require_authenticated_user
self.amq_configuration.amq_adapter.require_authenticated_user )
and isinstance(
self.amq_configuration.amq_adapter.require_authenticated_user, bool
)
or eval(
str(self.amq_configuration.amq_adapter.require_authenticated_user)
.strip()
.capitalize()
or "True"
)
)
except Exception as e:
self.require_authenticated_user = (
True # Enforce authenticated user as default
)
def get_content_type(self, message_headers: Dict[str, List[str]]) -> str: def get_content_type(self, message_headers: Dict[str, List[str]]) -> str:
""" """
+25 -10
View File
@@ -1,6 +1,5 @@
import configparser import configparser
import inspect import inspect
import logging
import os import os
from amqp.adapter.logging_utils import logging_error, logging_warning from amqp.adapter.logging_utils import logging_error, logging_warning
@@ -36,11 +35,16 @@ class AMQAdapter:
self.route_mapping = config.get( self.route_mapping = config.get(
"CleverMicro-AMQ", self.PREFIX + "route-mapping", fallback=None "CleverMicro-AMQ", self.PREFIX + "route-mapping", fallback=None
) )
self.require_authenticated_user = config.get( self.require_authenticated_user = config.getboolean(
"CleverMicro-AMQ", "CleverMicro-AMQ",
self.PREFIX + "require-authenticated-user", self.PREFIX + "require-authenticated-user",
fallback=False, fallback=False,
) )
# these 2 shall be provided by Docker swarm, format is a random alphanumeric string
self.swarm_service_id = os.getenv(
"SWARM_SERVICE_ID", default="clever-amqp-service"
)
self.swarm_task_id = os.getenv("SWARM_TASK_ID", default="python-adapter")
def adapter_prefix(self) -> str: def adapter_prefix(self) -> str:
return f"{self.service_name}-{self.generator_id}-" return f"{self.service_name}-{self.generator_id}-"
@@ -56,7 +60,6 @@ class Dispatch:
def __init__(self, config: configparser.ConfigParser): def __init__(self, config: configparser.ConfigParser):
""" """
cm.dispatch.use-dlq=true cm.dispatch.use-dlq=true
cm.dispatch.use-confirms=false
cm.dispatch.amq-host=rabbitmq cm.dispatch.amq-host=rabbitmq
cm.dispatch.amq-port=5672 cm.dispatch.amq-port=5672
cm.dispatch.amq-port-tls=5671 cm.dispatch.amq-port-tls=5671
@@ -68,9 +71,6 @@ class Dispatch:
self.use_dlq = config.getboolean( self.use_dlq = config.getboolean(
"CleverMicro-AMQ", self.PREFIX + "use-dlq", fallback=False "CleverMicro-AMQ", self.PREFIX + "use-dlq", fallback=False
) )
self.use_confirms = config.getboolean(
"CleverMicro-AMQ", self.PREFIX + "use-confirms", fallback=False
)
self.amq_host = config.get( self.amq_host = config.get(
"CleverMicro-AMQ", self.PREFIX + "amq-host", fallback="rabbitmq" "CleverMicro-AMQ", self.PREFIX + "amq-host", fallback="rabbitmq"
) )
@@ -110,12 +110,27 @@ class Backpressure:
:param config: config parser to read configuration values :param config: config parser to read configuration values
""" """
self.threshold = config.getint( self.threshold_threads = config.getint(
"CleverMicro-AMQ", self.PREFIX + "threshold", fallback=0 "CleverMicro-AMQ",
self.PREFIX + "threshold",
fallback=int(os.getenv("PARALLEL_WORKERS", 1)),
) )
# time-window is in milliseconds, to report backpressure IDLE alert # time-window in seconds, duration between backpressure reports
self.time_window = config.getint( self.time_window = config.getint(
"CleverMicro-AMQ", self.PREFIX + "time-window", fallback=10000 "CleverMicro-AMQ", self.PREFIX + "time-window", fallback=10
)
# Define maximum CPU usage before triggering backpressure OVERLOAD alert
self.threshold_cpu = config.getint(
"CleverMicro-AMQ", self.PREFIX + "threshold-cpu", fallback=90
)
# Define the time window length, in seconds, during which, if CPU usage is consistently above the threshold, the system will trigger OVERLOAD alert
self.cpu_overload_duration = config.getint(
"CleverMicro-AMQ", self.PREFIX + "cpu-overload-duration", fallback=30
)
# Define the time window length, in seconds, during which, if no request is made to the Service, the system will trigger IDLE alert
self.idle_duration = config.getint(
"CleverMicro-AMQ", self.PREFIX + "idle-duration", fallback=30
) )
+10 -1
View File
@@ -19,4 +19,13 @@ cm.dispatch.download-dir=/tmp/downloaded_files
#cm.dispatch.service-port=8080 #cm.dispatch.service-port=8080
# #
# Define maximum number of messages of being processed in parallel before triggering backpressure OVERLOAD alert # Define maximum number of messages of being processed in parallel before triggering backpressure OVERLOAD alert
cm.backpressure.threshold=20 cm.backpressure.threshold=5
# Define maximum CPU usage before triggering backpressure OVERLOAD alert
cm.backpressure.threshold-cpu=90
# Define the time window between the Backpressure reports, in seconds
cm.backpressure.time-window=10
# Define the time window length, in seconds, during which, if CPU usage is consistently above the threshold, the system will trigger OVERLOAD alert
cm.backpressure.cpu-overload-duration=30
# Define the time window length, in seconds, during which, if no request is made to the Service, the system will trigger IDLE alert
cm.backpressure.idle-duration=30
-19
View File
@@ -1,8 +1,6 @@
import asyncio import asyncio
import logging
import threading import threading
import time import time
import concurrent.futures
import os import os
from asyncio import Future, AbstractEventLoop from asyncio import Future, AbstractEventLoop
from typing import Dict from typing import Dict
@@ -30,11 +28,6 @@ from amqp.model.model import (
from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
from amqp.adapter.serializer import map_as_string from amqp.adapter.serializer import map_as_string
# Shared thread pool for parallel execution
executor = concurrent.futures.ThreadPoolExecutor(
max_workers=BackpressureHandler.parallel_workers + 2
)
def collect_trace_info(): def collect_trace_info():
trace_map = {} trace_map = {}
@@ -79,20 +72,8 @@ 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.file_handler: FileHandler = file_handler self.file_handler: FileHandler = file_handler
self.backpressure_handler: BackpressureHandler = backpressure_handler self.backpressure_handler: BackpressureHandler = backpressure_handler
_backpressure_treshold = amq_configuration.backpressure.threshold
"""
Determine the number of parallel workers based on the backpressure threshold and PARALLEL_WORKERS env var.
If env var PARALLEL_WORKERS is not set (or set to 0), use the backpressure threshold config
as the number of parallel workers.
"""
if _backpressure_treshold > 0:
if BackpressureHandler.parallel_workers <= 0:
BackpressureHandler.parallel_workers = _backpressure_treshold
async def inbound_data_message_callback(self, message: AbstractIncomingMessage): async def inbound_data_message_callback(self, message: AbstractIncomingMessage):
threading.Thread( threading.Thread(
+3 -1
View File
@@ -70,7 +70,9 @@ class AMQService:
loop loop
) )
backpressure_handler = BackpressureHandler( backpressure_handler = BackpressureHandler(
channel=self.rabbit_mq_client.channel, loop=loop channel=self.rabbit_mq_client.channel,
loop=loop,
config=self.amq_configuration,
) )
self.amq_data_message_handler = DataMessageHandler( self.amq_data_message_handler = DataMessageHandler(
service_adapter, service_adapter,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "amq_adapter" name = "amq_adapter"
version = "0.2.22-dev" version = "0.2.23-dev"
description = "The Python implementation of the AMQ Adaptor for CleverMicro" description = "The Python implementation of the AMQ Adaptor for CleverMicro"
readme = "README.md" readme = "README.md"
authors = [ authors = [