Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c6d7e9c003
|
|||
|
7e7d4e5b37
|
|||
|
d714399cfc
|
|||
| 0f8bec742d | |||
| d1ba4db460 |
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user