fix(General): fix availability report
Remove the loop for monitoring the CPU usage, adopt changes in the management service for the reporting message format, update the timing for sending out report, similar to java adapter. ISSUES RELATED: clevermicro/amq-adapter-python#58
This commit is contained in:
@@ -1,73 +0,0 @@
|
|||||||
name: Build and Publish Docker Image
|
|
||||||
|
|
||||||
#on:
|
|
||||||
# push:
|
|
||||||
# branches: [ main, master ]
|
|
||||||
# tags: [ 'v*' ]
|
|
||||||
# pull_request:
|
|
||||||
# branches: [ main, master ]
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # publish release on master
|
|
||||||
- develop # publish snapshot on develop
|
|
||||||
- feat-58-backpressure-reactive-streams
|
|
||||||
workflow_dispatch:
|
|
||||||
# allow manual trigger
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY_URL: "git.cleverthis.com"
|
|
||||||
REPOSITORY: "clevermicro/amq-adapter-python-demo"
|
|
||||||
DOCKER_HOST: "tcp://dind:2375"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
dind:
|
|
||||||
image: docker:dind
|
|
||||||
cmd:
|
|
||||||
- dockerd
|
|
||||||
- -H
|
|
||||||
- tcp://0.0.0.0:2375
|
|
||||||
- --tls=false
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY_URL }}
|
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY_URL }}/clevermicro/amq-adapter-python-demo
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=sha,format=short
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
# push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.REGISTRY_URL }}/${{ env.REPOSITORY }}:latest
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
@@ -38,4 +38,4 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
no-cache: true
|
no-cache: true
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.REGISTRY_URL }}/${{ env.REPOSITORY }}:20250709-01
|
tags: ${{ env.REGISTRY_URL }}/${{ env.REPOSITORY }}:20250715-01
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import time
|
|||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
import psutil
|
|
||||||
from aio_pika import Message
|
from aio_pika import Message
|
||||||
from aio_pika.abc import AbstractRobustChannel
|
from aio_pika.abc import AbstractRobustChannel
|
||||||
|
|
||||||
from amqp.adapter.logging_utils import logging_info, logging_warning
|
from amqp.adapter.logging_utils import logging_info
|
||||||
from amqp.config.amq_configuration import AMQConfiguration
|
from amqp.config.amq_configuration import AMQConfiguration
|
||||||
from amqp.model.model import ScalingRequestAlert
|
|
||||||
from amqp.router.utils import await_future, await_result
|
from amqp.router.utils import await_future, await_result
|
||||||
|
|
||||||
CPU_IDLE, CPU_ACTIVE, CPU_OVERLOAD = 0, 1, 2
|
CPU_IDLE, CPU_ACTIVE, CPU_OVERLOAD = 0, 1, 2
|
||||||
@@ -24,16 +22,12 @@ class ScaleRequestV1:
|
|||||||
self,
|
self,
|
||||||
serviceId: str,
|
serviceId: str,
|
||||||
taskId: str,
|
taskId: str,
|
||||||
maxAvailability: int,
|
|
||||||
currentAvailability: int,
|
currentAvailability: int,
|
||||||
requestType: ScalingRequestAlert,
|
|
||||||
):
|
):
|
||||||
self.version = 1 # Version of the request, currently 1
|
self.version = 1 # Version of the request, currently 1
|
||||||
self.serviceId = serviceId
|
self.serviceId = serviceId
|
||||||
self.taskId = taskId
|
self.taskId = taskId
|
||||||
self.maxAvailability = maxAvailability
|
|
||||||
self.currentAvailability = currentAvailability
|
self.currentAvailability = currentAvailability
|
||||||
self.requestType = requestType
|
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
@@ -43,9 +37,7 @@ class ScaleRequestV1:
|
|||||||
"version": self.version,
|
"version": self.version,
|
||||||
"serviceId": self.serviceId,
|
"serviceId": self.serviceId,
|
||||||
"taskId": self.taskId,
|
"taskId": self.taskId,
|
||||||
"maxAvailability": self.maxAvailability,
|
|
||||||
"currentAvailability": self.currentAvailability,
|
"currentAvailability": self.currentAvailability,
|
||||||
"requestType": self.requestType.value, # Using .value for Enum serialization
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
)
|
)
|
||||||
@@ -95,11 +87,10 @@ class RunningAverage:
|
|||||||
class BackpressureHandler:
|
class BackpressureHandler:
|
||||||
# Track the number of messages currently being processed
|
# Track the number of messages currently being processed
|
||||||
current_parallel_executions = 0
|
current_parallel_executions = 0
|
||||||
# helps detect IDLE condition
|
# report when remain availability goes down
|
||||||
last_data_message_time = 0
|
last_usage_report_time = 0
|
||||||
# helps prevent flooding the system with backpressure events
|
# report when remain availability goes up
|
||||||
last_backpressure_event_time = 0
|
last_recover_report_time = 0
|
||||||
last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
# _callback_list is used in unit tests to record the invoked callbacks
|
# _callback_list is used in unit tests to record the invoked callbacks
|
||||||
_callback_list = None
|
_callback_list = None
|
||||||
|
|
||||||
@@ -117,12 +108,7 @@ class BackpressureHandler:
|
|||||||
self.swarm_service_id = self.config.amq_adapter.swarm_service_id
|
self.swarm_service_id = self.config.amq_adapter.swarm_service_id
|
||||||
self.swarm_task_id = self.config.amq_adapter.swarm_task_id
|
self.swarm_task_id = self.config.amq_adapter.swarm_task_id
|
||||||
self.parallel_workers = self.config.backpressure.threshold_threads
|
self.parallel_workers = self.config.backpressure.threshold_threads
|
||||||
self.average_cpu_usage = None
|
|
||||||
self.do_loop = -1
|
self.do_loop = -1
|
||||||
self._resource_usage_state = RESOURCE_UNSET
|
|
||||||
self._resource_usage_changed = 0
|
|
||||||
self._resource_average_value = 0
|
|
||||||
self._last_resource_max_value = 100 # Default max value for CPU usage
|
|
||||||
|
|
||||||
def _do_loop(self) -> bool:
|
def _do_loop(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -135,177 +121,66 @@ class BackpressureHandler:
|
|||||||
self.do_loop -= 1
|
self.do_loop -= 1
|
||||||
return _val
|
return _val
|
||||||
|
|
||||||
def increase_parallel_executions(self):
|
async def increase_parallel_executions(self):
|
||||||
"""Increase the number of parallel executions"""
|
"""Increase the number of parallel executions"""
|
||||||
logging_info(
|
|
||||||
"Backpressure: Increase parallel executions, current=%s",
|
|
||||||
self.current_parallel_executions,
|
|
||||||
)
|
|
||||||
self.current_parallel_executions += 1
|
self.current_parallel_executions += 1
|
||||||
|
|
||||||
def decrease_parallel_executions(self):
|
|
||||||
"""Decrease the number of parallel executions"""
|
|
||||||
logging_info(
|
logging_info(
|
||||||
"Backpressure: Decrease parallel executions, current=%s",
|
"Backpressure: Increased parallel executions, new value: %s",
|
||||||
self.current_parallel_executions,
|
self.current_parallel_executions,
|
||||||
)
|
)
|
||||||
|
# increase parallel -> reduce in remaining availability -> usage
|
||||||
|
# report usage at least every 1s
|
||||||
|
# TODO: add config for the interval
|
||||||
|
if self.last_usage_report_time + 1 < time.time():
|
||||||
|
await self.send_backpressure_report()
|
||||||
|
self.last_usage_report_time = time.time()
|
||||||
|
|
||||||
|
async def decrease_parallel_executions(self):
|
||||||
|
"""Decrease the number of parallel executions"""
|
||||||
if self.current_parallel_executions > 0:
|
if self.current_parallel_executions > 0:
|
||||||
self.current_parallel_executions -= 1
|
self.current_parallel_executions -= 1
|
||||||
|
logging_info(
|
||||||
def update_parallel_executions(self, count: int):
|
"Backpressure: Decreased parallel executions, new value=%s",
|
||||||
"""Update the number of parallel executions"""
|
self.current_parallel_executions,
|
||||||
self.current_parallel_executions = count
|
)
|
||||||
|
# decrease parallel -> add to remaining availability -> recover
|
||||||
def update_last_data_message_time(self):
|
# report recover at every 3s
|
||||||
logging_info("Backpressure: Update last data message time")
|
# TODO: add config for the interval
|
||||||
"""Update the last data message time"""
|
if self.last_recover_report_time + 3 < time.time():
|
||||||
self.last_data_message_time = time.time()
|
await self.send_backpressure_report()
|
||||||
|
self.last_recover_report_time = time.time()
|
||||||
|
|
||||||
def start_backpressure_monitor(self) -> Thread:
|
def start_backpressure_monitor(self) -> Thread:
|
||||||
# Start the Backpressure monitor loop
|
# Start the Backpressure monitor loop
|
||||||
self.thread = Thread(target=self.backpressure_monitor_loop)
|
self.thread = Thread(target=self.regular_report_loop_wrapper)
|
||||||
self.thread.daemon = True # This makes it a daemon thread
|
self.thread.daemon = True # This makes it a daemon thread
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
return self.thread
|
return self.thread
|
||||||
|
|
||||||
async def check_overload_condition(self):
|
async def _regular_report_loop(self):
|
||||||
"""Check if the current parallel executions exceed the limit"""
|
"""Regularly report the availability in a fixed interval"""
|
||||||
self.update_last_data_message_time()
|
|
||||||
logging_info(
|
|
||||||
"Backpressure: Check overload condition, current=%s, max=%s",
|
|
||||||
self.current_parallel_executions,
|
|
||||||
self.parallel_workers,
|
|
||||||
)
|
|
||||||
if self.current_parallel_executions >= self.parallel_workers - 1:
|
|
||||||
# Check if the last backpressure event was not an overload
|
|
||||||
if self.last_backpressure_event != ScalingRequestAlert.OVERLOAD:
|
|
||||||
# Trigger the overload event
|
|
||||||
await self.handle_backpressure_overload_event()
|
|
||||||
self.last_backpressure_event_time = time.time()
|
|
||||||
self.last_backpressure_event = ScalingRequestAlert.OVERLOAD
|
|
||||||
|
|
||||||
async def _backpressure_monitor(self):
|
|
||||||
"""Monitor the backpressure conditions and trigger events accordingly"""
|
|
||||||
_time_window_millis = self.config.backpressure.time_window
|
|
||||||
_idle_duration_millis = self.config.backpressure.idle_duration
|
|
||||||
_overload_duration_millis = self.config.backpressure.cpu_overload_duration
|
|
||||||
_monitor_interval = 0.5 # Monitor every 500ms
|
|
||||||
CPU_IDLE, CPU_ACTIVE, CPU_OVERLOAD = 0, 1, 2
|
|
||||||
IDLE_THRESHOLD, OVERLOAD_THRESHOLD = 15, 90
|
|
||||||
if self.average_cpu_usage is None:
|
|
||||||
self.average_cpu_usage = RunningAverage(
|
|
||||||
int(max(_overload_duration_millis, _idle_duration_millis) // _monitor_interval)
|
|
||||||
)
|
|
||||||
_cpu_usage_state = CPU_ACTIVE
|
|
||||||
_cpu_usage_changed = time.time()
|
|
||||||
while self._do_loop():
|
while self._do_loop():
|
||||||
_current_cpu_usage = psutil.cpu_percent(interval=None)
|
await self.send_backpressure_report()
|
||||||
self.average_cpu_usage.add(_current_cpu_usage)
|
# report recover at every 5s
|
||||||
_average_cpu_usage = self.average_cpu_usage.get_average()
|
# TODO: add config for the interval
|
||||||
_now = time.time()
|
await asyncio.sleep(5)
|
||||||
# logging_info(
|
|
||||||
# "Backpressure: current CPU[pct]=%s, avg=%s, state=%s, last dataMsg=%s, last event=%s, last eventTime=%s",
|
|
||||||
# _current_cpu_usage,
|
|
||||||
# _average_cpu_usage,
|
|
||||||
# _cpu_usage_state,
|
|
||||||
# self.last_data_message_time,
|
|
||||||
# self.last_backpressure_event,
|
|
||||||
# self.last_backpressure_event_time,
|
|
||||||
# )
|
|
||||||
if _average_cpu_usage < IDLE_THRESHOLD:
|
|
||||||
if _cpu_usage_state != CPU_IDLE:
|
|
||||||
_cpu_usage_changed = _now
|
|
||||||
_cpu_usage_state = CPU_IDLE
|
|
||||||
elif _average_cpu_usage > OVERLOAD_THRESHOLD:
|
|
||||||
if _cpu_usage_state != CPU_OVERLOAD:
|
|
||||||
_cpu_usage_changed = _now
|
|
||||||
_cpu_usage_state = CPU_OVERLOAD
|
|
||||||
else:
|
|
||||||
if _cpu_usage_state != CPU_ACTIVE:
|
|
||||||
_cpu_usage_changed = _now
|
|
||||||
_cpu_usage_state = CPU_ACTIVE
|
|
||||||
|
|
||||||
# Check if the current time exceeds the last backpressure event time
|
def regular_report_loop_wrapper(self):
|
||||||
if (
|
|
||||||
_cpu_usage_state == CPU_OVERLOAD
|
|
||||||
and _now - _cpu_usage_changed > _overload_duration_millis
|
|
||||||
and (
|
|
||||||
_now - self.last_backpressure_event_time > _time_window_millis
|
|
||||||
or self.last_backpressure_event != ScalingRequestAlert.OVERLOAD
|
|
||||||
)
|
|
||||||
):
|
|
||||||
# Trigger the overload event
|
|
||||||
await self.handle_backpressure_overload_event()
|
|
||||||
self.last_backpressure_event_time = _now
|
|
||||||
self.last_backpressure_event = ScalingRequestAlert.OVERLOAD
|
|
||||||
elif (
|
|
||||||
_cpu_usage_state == CPU_IDLE
|
|
||||||
and _now - _cpu_usage_changed > _idle_duration_millis
|
|
||||||
and _now - self.last_data_message_time > _idle_duration_millis
|
|
||||||
and (
|
|
||||||
_now - self.last_backpressure_event_time > _time_window_millis
|
|
||||||
or self.last_backpressure_event != ScalingRequestAlert.IDLE
|
|
||||||
)
|
|
||||||
):
|
|
||||||
# Trigger the idle event
|
|
||||||
await self._handle_backpressure_idle_event()
|
|
||||||
self.last_backpressure_event = ScalingRequestAlert.IDLE
|
|
||||||
self.last_backpressure_event_time = _now
|
|
||||||
else:
|
|
||||||
# Trigger update event
|
|
||||||
if _now - self.last_backpressure_event_time > _time_window_millis:
|
|
||||||
# Trigger the update event
|
|
||||||
_scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
|
||||||
self.swarm_service_id,
|
|
||||||
self.swarm_task_id,
|
|
||||||
100,
|
|
||||||
_average_cpu_usage,
|
|
||||||
ScalingRequestAlert.UPDATE,
|
|
||||||
)
|
|
||||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
|
||||||
await self.publish_backpressure_request(_scaling_request)
|
|
||||||
self.last_backpressure_event_time = _now
|
|
||||||
self.last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
await asyncio.sleep(_monitor_interval)
|
|
||||||
|
|
||||||
def backpressure_monitor_loop(self):
|
|
||||||
_loop = asyncio.new_event_loop()
|
_loop = asyncio.new_event_loop()
|
||||||
_loop.run_until_complete(self._backpressure_monitor())
|
_loop.run_until_complete(self._regular_report_loop())
|
||||||
|
|
||||||
async def handle_backpressure_overload_event(self):
|
async def send_backpressure_report(self):
|
||||||
logging_warning("Backpressure: Capacity close to depleted!")
|
|
||||||
# Send an Overload event to the Management service
|
|
||||||
scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
||||||
self.swarm_service_id,
|
serviceId=self.swarm_service_id,
|
||||||
self.swarm_task_id,
|
taskId=self.swarm_task_id,
|
||||||
self.parallel_workers,
|
currentAvailability=self.parallel_workers - self.current_parallel_executions,
|
||||||
self.parallel_workers - self.current_parallel_executions,
|
|
||||||
ScalingRequestAlert.OVERLOAD,
|
|
||||||
)
|
)
|
||||||
# Address the message to any management service instance capable of supporting BACKPRESSURE request
|
|
||||||
await self.publish_backpressure_request(scaling_request)
|
await self.publish_backpressure_request(scaling_request)
|
||||||
# update / reset time-window so that the OVERLOAD is not sent too often
|
|
||||||
self.last_data_message_time = time.time()
|
|
||||||
|
|
||||||
async def _handle_backpressure_idle_event(self):
|
|
||||||
logging_warning("Backpressure: Service is idle.")
|
|
||||||
# Send an Idle event to the Management service
|
|
||||||
scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
|
||||||
self.swarm_service_id,
|
|
||||||
self.swarm_task_id,
|
|
||||||
self.parallel_workers,
|
|
||||||
self.parallel_workers - self.current_parallel_executions,
|
|
||||||
ScalingRequestAlert.IDLE,
|
|
||||||
)
|
|
||||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
|
||||||
await self.publish_backpressure_request(scaling_request)
|
|
||||||
# update / reset time-window so that the IDLE is not sent too often
|
|
||||||
self.last_data_message_time = time.time()
|
|
||||||
|
|
||||||
async def publish_backpressure_request(self, scaling_request: ScaleRequestV1):
|
async def publish_backpressure_request(self, scaling_request: ScaleRequestV1):
|
||||||
# Publish the backpressure request to the management service
|
# Publish the backpressure request to the management service
|
||||||
logging_info(
|
logging_info(
|
||||||
f"Publishing backpressure for {scaling_request.serviceId} with request type {scaling_request.requestType}"
|
f"Publishing backpressure for {scaling_request.serviceId} with availability {scaling_request.currentAvailability}"
|
||||||
)
|
)
|
||||||
if not self.exchange:
|
if not self.exchange:
|
||||||
|
|
||||||
|
|||||||
@@ -291,21 +291,11 @@ class ServiceMessage:
|
|||||||
return self.message_type is not None
|
return self.message_type is not None
|
||||||
|
|
||||||
|
|
||||||
class ScalingRequestAlert(Enum):
|
|
||||||
OVERLOAD = "OVERLOAD"
|
|
||||||
IDLE = "IDLE"
|
|
||||||
UPDATE = "UPDATE"
|
|
||||||
SCALE_UP = "SCALE_UP"
|
|
||||||
SCALE_DOWN = "SCALE_DOWN"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ScalingRequest:
|
class ScalingRequest:
|
||||||
service_id: str
|
service_id: str
|
||||||
task_id: str
|
task_id: str
|
||||||
max_availability: int
|
|
||||||
current_availability: int
|
current_availability: int
|
||||||
request_type: ScalingRequestAlert
|
|
||||||
version: int = 1
|
version: int = 1
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
@@ -315,9 +305,7 @@ class ScalingRequest:
|
|||||||
"version": self.version,
|
"version": self.version,
|
||||||
"serviceId": self.service_id,
|
"serviceId": self.service_id,
|
||||||
"taskId": self.task_id,
|
"taskId": self.task_id,
|
||||||
"maxAvailability": self.max_availability,
|
|
||||||
"currentAvailability": self.current_availability,
|
"currentAvailability": self.current_availability,
|
||||||
"requestType": self.request_type.value, # Using .value for Enum serialization
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
)
|
)
|
||||||
@@ -329,9 +317,7 @@ class ScalingRequest:
|
|||||||
return cls(
|
return cls(
|
||||||
service_id=data["serviceId"],
|
service_id=data["serviceId"],
|
||||||
task_id=data["taskId"],
|
task_id=data["taskId"],
|
||||||
max_availability=data["maxAvailability"],
|
|
||||||
current_availability=data["currentAvailability"],
|
current_availability=data["currentAvailability"],
|
||||||
request_type=ScalingRequestAlert(data["requestType"]), # Convert string to Enum
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,9 +123,8 @@ class DataMessageHandler:
|
|||||||
:param loop: Event loop
|
:param loop: Event loop
|
||||||
:return: DataResponse
|
:return: DataResponse
|
||||||
"""
|
"""
|
||||||
# Increment the counter for parallel executions and check resource availability
|
# Increment the counter for parallel executions
|
||||||
self.backpressure_handler.increase_parallel_executions()
|
await self.backpressure_handler.increase_parallel_executions()
|
||||||
await self.backpressure_handler.check_overload_condition()
|
|
||||||
logging_info(
|
logging_info(
|
||||||
f"PARALLEL: Current Capacity [{self.backpressure_handler.current_parallel_executions}] of [{self.backpressure_handler.parallel_workers}]"
|
f"PARALLEL: Current Capacity [{self.backpressure_handler.current_parallel_executions}] of [{self.backpressure_handler.parallel_workers}]"
|
||||||
)
|
)
|
||||||
@@ -154,9 +153,8 @@ class DataMessageHandler:
|
|||||||
logging_error(f"Deleting file {filename}: {e}")
|
logging_error(f"Deleting file {filename}: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging_error(f"Request handler: {e}")
|
logging_error(f"Request handler: {e}")
|
||||||
|
# recover availability
|
||||||
self.backpressure_handler.decrease_parallel_executions()
|
await self.backpressure_handler.decrease_parallel_executions()
|
||||||
await self.backpressure_handler.check_overload_condition()
|
|
||||||
|
|
||||||
async def reconstitute_data_message(
|
async def reconstitute_data_message(
|
||||||
self, message: AbstractIncomingMessage
|
self, message: AbstractIncomingMessage
|
||||||
|
|||||||
@@ -79,25 +79,6 @@ class AMQService:
|
|||||||
logging.info("***********************************************")
|
logging.info("***********************************************")
|
||||||
self.loop.run_forever()
|
self.loop.run_forever()
|
||||||
|
|
||||||
def backpressure(self, current: int, maximum: int = -1):
|
|
||||||
"""
|
|
||||||
Send Backpressure event to the Management Service. The Backpressure logic determines the type of the event
|
|
||||||
based on the current and maximum values.
|
|
||||||
- OVERLOAD event is triggered immediately when the current value exceeds the maximum threshold, which is
|
|
||||||
typically set to 80% of the maximum value.
|
|
||||||
- IDLE event is triggered when the current value drops below 20% of the maximum value and remains there for
|
|
||||||
a IDLE_WINDOW period of time (see BackpressureHandler).
|
|
||||||
- UPDATE event is sent if neither OVERLOAD nor IDLE conditions are met, indicating that the backpressure is
|
|
||||||
within acceptable limits.
|
|
||||||
:param current: Current value of the backpressure metric.
|
|
||||||
:param maximum: Maximum value of the backpressure metric. If undefined or set to lt 0, the maximum is set
|
|
||||||
according to the configuration value 'cm.backpressure.threshold'.
|
|
||||||
"""
|
|
||||||
if self.backpressure_handler is not None:
|
|
||||||
if maximum < 0:
|
|
||||||
maximum = self.amq_configuration.backpressure.threshold_threads
|
|
||||||
self.backpressure_handler.update_backpressure_value(current, maximum)
|
|
||||||
|
|
||||||
def rpc(self, service_name: str, fq_method_name: str, **kwargs) -> Future:
|
def rpc(self, service_name: str, fq_method_name: str, **kwargs) -> Future:
|
||||||
"""
|
"""
|
||||||
Send a Remote Procedure Call (RPC) message to the AMQ service.
|
Send a Remote Procedure Call (RPC) message to the AMQ service.
|
||||||
|
|||||||
@@ -13,15 +13,11 @@ from aio_pika import Exchange
|
|||||||
from aio_pika.abc import AbstractRobustChannel
|
from aio_pika.abc import AbstractRobustChannel
|
||||||
|
|
||||||
from amqp.adapter.backpressure_handler import (
|
from amqp.adapter.backpressure_handler import (
|
||||||
RESOURCE_ACTIVE,
|
|
||||||
RESOURCE_OVERLOAD,
|
|
||||||
RESOURCE_UNSET,
|
|
||||||
BackpressureHandler,
|
BackpressureHandler,
|
||||||
RunningAverage,
|
RunningAverage,
|
||||||
ScaleRequestV1,
|
ScaleRequestV1,
|
||||||
)
|
)
|
||||||
from amqp.config.amq_configuration import AMQConfiguration
|
from amqp.config.amq_configuration import AMQConfiguration
|
||||||
from amqp.model.model import ScalingRequestAlert
|
|
||||||
|
|
||||||
|
|
||||||
class TestScaleRequestV1:
|
class TestScaleRequestV1:
|
||||||
@@ -35,24 +31,18 @@ class TestScaleRequestV1:
|
|||||||
"""
|
"""
|
||||||
service_id = "test-service"
|
service_id = "test-service"
|
||||||
task_id = "test-task"
|
task_id = "test-task"
|
||||||
max_availability = 10
|
|
||||||
current_availability = 5
|
current_availability = 5
|
||||||
request_type = ScalingRequestAlert.UPDATE
|
|
||||||
|
|
||||||
scale_request = ScaleRequestV1(
|
scale_request = ScaleRequestV1(
|
||||||
service_id,
|
service_id,
|
||||||
task_id,
|
task_id,
|
||||||
max_availability,
|
|
||||||
current_availability,
|
current_availability,
|
||||||
request_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert scale_request.version == 1
|
assert scale_request.version == 1
|
||||||
assert scale_request.serviceId == service_id
|
assert scale_request.serviceId == service_id
|
||||||
assert scale_request.taskId == task_id
|
assert scale_request.taskId == task_id
|
||||||
assert scale_request.maxAvailability == max_availability
|
|
||||||
assert scale_request.currentAvailability == current_availability
|
assert scale_request.currentAvailability == current_availability
|
||||||
assert scale_request.requestType == request_type
|
|
||||||
assert scale_request.thread is None
|
assert scale_request.thread is None
|
||||||
|
|
||||||
def test_scale_request_v1_to_json(self):
|
def test_scale_request_v1_to_json(self):
|
||||||
@@ -61,16 +51,12 @@ class TestScaleRequestV1:
|
|||||||
"""
|
"""
|
||||||
service_id = "test-service"
|
service_id = "test-service"
|
||||||
task_id = "test-task"
|
task_id = "test-task"
|
||||||
max_availability = 10
|
|
||||||
current_availability = 5
|
current_availability = 5
|
||||||
request_type = ScalingRequestAlert.UPDATE
|
|
||||||
|
|
||||||
scale_request = ScaleRequestV1(
|
scale_request = ScaleRequestV1(
|
||||||
service_id,
|
service_id,
|
||||||
task_id,
|
task_id,
|
||||||
max_availability,
|
|
||||||
current_availability,
|
current_availability,
|
||||||
request_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
json_output = scale_request.to_json()
|
json_output = scale_request.to_json()
|
||||||
@@ -79,9 +65,7 @@ class TestScaleRequestV1:
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"serviceId": service_id,
|
"serviceId": service_id,
|
||||||
"taskId": task_id,
|
"taskId": task_id,
|
||||||
"maxAvailability": max_availability,
|
|
||||||
"currentAvailability": current_availability,
|
"currentAvailability": current_availability,
|
||||||
"requestType": request_type.value,
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
)
|
)
|
||||||
@@ -93,16 +77,12 @@ class TestScaleRequestV1:
|
|||||||
"""
|
"""
|
||||||
service_id = "test-service"
|
service_id = "test-service"
|
||||||
task_id = "test-task"
|
task_id = "test-task"
|
||||||
max_availability = 10
|
|
||||||
current_availability = 5
|
current_availability = 5
|
||||||
request_type = ScalingRequestAlert.OVERLOAD # Change the alert type
|
|
||||||
|
|
||||||
scale_request = ScaleRequestV1(
|
scale_request = ScaleRequestV1(
|
||||||
service_id,
|
service_id,
|
||||||
task_id,
|
task_id,
|
||||||
max_availability,
|
|
||||||
current_availability,
|
current_availability,
|
||||||
request_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
json_output = scale_request.to_json()
|
json_output = scale_request.to_json()
|
||||||
@@ -111,9 +91,7 @@ class TestScaleRequestV1:
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"serviceId": service_id,
|
"serviceId": service_id,
|
||||||
"taskId": task_id,
|
"taskId": task_id,
|
||||||
"maxAvailability": max_availability,
|
|
||||||
"currentAvailability": current_availability,
|
"currentAvailability": current_availability,
|
||||||
"requestType": request_type.value, # Ensure the value is correctly serialized
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
)
|
)
|
||||||
@@ -288,47 +266,43 @@ class TestBackpressureHandler:
|
|||||||
assert handler.swarm_service_id == "clever-amqp-service"
|
assert handler.swarm_service_id == "clever-amqp-service"
|
||||||
assert handler.swarm_task_id == "python-adapter"
|
assert handler.swarm_task_id == "python-adapter"
|
||||||
assert handler.parallel_workers == mock_config.backpressure.threshold_threads
|
assert handler.parallel_workers == mock_config.backpressure.threshold_threads
|
||||||
assert handler.average_cpu_usage is None
|
|
||||||
|
|
||||||
def test_increase_parallel_executions(self, mock_channel, mock_loop, mock_config):
|
@pytest.mark.asyncio
|
||||||
|
async def test_increase_parallel_executions(self, mock_channel, mock_loop, mock_config):
|
||||||
"""
|
"""
|
||||||
Test the increase_parallel_executions method.
|
Test the increase_parallel_executions method.
|
||||||
"""
|
"""
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
||||||
|
handler.send_backpressure_report = AsyncMock()
|
||||||
initial_executions = handler.current_parallel_executions
|
initial_executions = handler.current_parallel_executions
|
||||||
handler.increase_parallel_executions()
|
|
||||||
assert handler.current_parallel_executions == initial_executions + 1
|
|
||||||
|
|
||||||
def test_decrease_parallel_executions(self, mock_channel, mock_loop, mock_config):
|
with patch("time.time") as mock_time:
|
||||||
|
mock_time.return_value = 12345
|
||||||
|
await handler.increase_parallel_executions()
|
||||||
|
assert handler.current_parallel_executions == initial_executions + 1
|
||||||
|
assert handler.last_usage_report_time == 12345
|
||||||
|
assert handler.send_backpressure_report.call_count == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_decrease_parallel_executions(self, mock_channel, mock_loop, mock_config):
|
||||||
"""
|
"""
|
||||||
Test the decrease_parallel_executions method.
|
Test the decrease_parallel_executions method.
|
||||||
"""
|
"""
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
||||||
|
handler.send_backpressure_report = AsyncMock()
|
||||||
handler.current_parallel_executions = 5
|
handler.current_parallel_executions = 5
|
||||||
handler.decrease_parallel_executions()
|
|
||||||
assert handler.current_parallel_executions == 4
|
|
||||||
|
|
||||||
handler.current_parallel_executions = 0
|
|
||||||
handler.decrease_parallel_executions()
|
|
||||||
assert handler.current_parallel_executions == 0 # Should not go below 0
|
|
||||||
|
|
||||||
def test_update_parallel_executions(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the update_parallel_executions method.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.update_parallel_executions(100)
|
|
||||||
assert handler.current_parallel_executions == 100
|
|
||||||
|
|
||||||
def test_update_last_data_message_time(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the update_last_data_message_time method.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
with patch("time.time") as mock_time:
|
with patch("time.time") as mock_time:
|
||||||
mock_time.return_value = 12345
|
mock_time.return_value = 12345
|
||||||
handler.update_last_data_message_time()
|
await handler.decrease_parallel_executions()
|
||||||
assert handler.last_data_message_time == 12345
|
assert handler.current_parallel_executions == 4
|
||||||
|
assert handler.last_recover_report_time == 12345
|
||||||
|
assert handler.send_backpressure_report.call_count == 1
|
||||||
|
|
||||||
|
handler.last_recover_report_time = 0
|
||||||
|
handler.current_parallel_executions = 0
|
||||||
|
await handler.decrease_parallel_executions()
|
||||||
|
assert handler.current_parallel_executions == 0 # Should not go below 0
|
||||||
|
|
||||||
def test_start_backpressure_monitor(self, mock_channel, mock_loop, mock_config):
|
def test_start_backpressure_monitor(self, mock_channel, mock_loop, mock_config):
|
||||||
"""
|
"""
|
||||||
@@ -341,222 +315,29 @@ class TestBackpressureHandler:
|
|||||||
assert handler.thread == thread
|
assert handler.thread == thread
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_overload_condition_no_overload(self, mock_channel, mock_loop, mock_config):
|
async def test_regular_report_loop(self, mock_channel, mock_loop, mock_config):
|
||||||
"""
|
"""
|
||||||
Test the check_overload_condition method when there is no overload.
|
Test the _regular_report_loop method.
|
||||||
"""
|
"""
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
||||||
handler.current_parallel_executions = 1
|
handler.send_backpressure_report = AsyncMock()
|
||||||
handler.parallel_workers = 5
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
with patch.object(handler, "handle_backpressure_overload_event") as mock_handle_overload:
|
|
||||||
await handler.check_overload_condition()
|
|
||||||
mock_handle_overload.assert_not_called()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
# test 1 loop
|
||||||
async def test_check_overload_condition_overload(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the check_overload_condition method when there is an overload.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.current_parallel_executions = 9 # Assuming parallel_workers is 10
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
with patch.object(handler, "handle_backpressure_overload_event") as mock_handle_overload:
|
|
||||||
with patch("time.time") as mock_time:
|
|
||||||
mock_time.return_value = 12345
|
|
||||||
await handler.check_overload_condition()
|
|
||||||
mock_handle_overload.assert_called_once()
|
|
||||||
assert handler.last_backpressure_event_time == 12345
|
|
||||||
assert handler.last_backpressure_event == ScalingRequestAlert.OVERLOAD
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_check_overload_condition_already_overload(
|
|
||||||
self, mock_channel, mock_loop, mock_config
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Test the check_overload_condition method when the last event was already an overload.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.current_parallel_executions = 10
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.OVERLOAD
|
|
||||||
with patch.object(handler, "handle_backpressure_overload_event") as mock_handle_overload:
|
|
||||||
await handler.check_overload_condition()
|
|
||||||
mock_handle_overload.assert_not_called()
|
|
||||||
|
|
||||||
# backpressure_monitor_loop is difficult to test directly without significant mocking
|
|
||||||
# and potentially long-running tests. The following is a basic outline of how
|
|
||||||
# you might approach testing it, focusing on verifying that the correct methods
|
|
||||||
# are called under specific conditions. This is NOT a complete, runnable test.
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_backpressure_monitor_loop(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the backpressure_monitor_loop method (outline).
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.average_cpu_usage = RunningAverage(
|
|
||||||
100, initial_value=99
|
|
||||||
) # Mock the RunningAverage in OVERLOAD state
|
|
||||||
handler.average_cpu_usage.is_filled = True # Together with 0 values ensures OVERLOAD state
|
|
||||||
# Setup mocks for dependent methods
|
|
||||||
handler.handle_backpressure_overload_event = AsyncMock()
|
|
||||||
handler._handle_backpressure_idle_event = AsyncMock()
|
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
|
||||||
|
|
||||||
# 1. Simulate an overload condition:
|
|
||||||
handler.last_backpressure_event_time = 0 # Force event trigger
|
|
||||||
handler.last_data_message_time = 0
|
|
||||||
handler.do_loop = 1
|
handler.do_loop = 1
|
||||||
handler.config.backpressure.cpu_overload_duration = (
|
await handler._regular_report_loop()
|
||||||
-1
|
handler.send_backpressure_report.assert_called_once()
|
||||||
) # Force overload condition even for recent change
|
|
||||||
await handler._backpressure_monitor()
|
|
||||||
handler.handle_backpressure_overload_event.assert_called_once()
|
|
||||||
|
|
||||||
# 2. Simulate an idle condition
|
|
||||||
handler.average_cpu_usage = RunningAverage(
|
|
||||||
100, initial_value=0
|
|
||||||
) # Mock the RunningAverage in IDLE state
|
|
||||||
handler.average_cpu_usage.is_filled = True # Together with 0 values ensures IDLE state
|
|
||||||
handler.last_backpressure_event_time = 0
|
|
||||||
handler.last_data_message_time = 0
|
|
||||||
handler.do_loop = 1
|
|
||||||
handler.config.backpressure.idle_duration = (
|
|
||||||
-1
|
|
||||||
) # Force idle condition even for recent change
|
|
||||||
await handler._backpressure_monitor()
|
|
||||||
handler._handle_backpressure_idle_event.assert_called_once()
|
|
||||||
|
|
||||||
# 3. Simulate neither idle nor overload, but time_window passed
|
|
||||||
handler.handle_backpressure_overload_event.call_count = 0
|
|
||||||
handler._handle_backpressure_idle_event.call_count = 0
|
|
||||||
handler.average_cpu_usage = RunningAverage(100) # Mock the RunningAverage in pritine state
|
|
||||||
handler.last_backpressure_event_time = 0
|
|
||||||
handler.last_data_message_time = 0
|
|
||||||
handler.do_loop = 1
|
|
||||||
await handler._backpressure_monitor()
|
|
||||||
assert (
|
|
||||||
handler.publish_backpressure_request.call_count
|
|
||||||
+ handler.handle_backpressure_overload_event.call_count
|
|
||||||
+ handler._handle_backpressure_idle_event.call_count
|
|
||||||
) > 0
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_handle_backpressure_overload_event(self, mock_channel, mock_loop, mock_config):
|
async def test_send_backpressure_report(self, mock_channel, mock_loop, mock_config):
|
||||||
"""
|
"""
|
||||||
Test the handle_backpressure_overload_event method.
|
Test the send_backpressure_report method.
|
||||||
"""
|
"""
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
handler.publish_backpressure_request = AsyncMock()
|
||||||
with patch("time.time") as mock_time:
|
with patch("time.time") as mock_time:
|
||||||
mock_time.return_value = 12345
|
mock_time.return_value = 12345
|
||||||
await handler.handle_backpressure_overload_event()
|
await handler.send_backpressure_report()
|
||||||
assert handler.publish_backpressure_request.call_count == 1
|
assert handler.publish_backpressure_request.call_count == 1
|
||||||
assert handler.last_data_message_time == 12345
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_backpressure_idle_event(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the _handle_backpressure_idle_event method.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
|
||||||
with patch("time.time") as mock_time:
|
|
||||||
mock_time.return_value = 12345
|
|
||||||
await handler._handle_backpressure_idle_event()
|
|
||||||
assert handler.publish_backpressure_request.call_count == 1
|
|
||||||
assert handler.last_data_message_time == 12345
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_check_resource_availability_overload(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the check_resource_availability method when resource usage is in OVERLOAD state.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.handle_backpressure_overload_event = AsyncMock()
|
|
||||||
handler._handle_backpressure_idle_event = AsyncMock()
|
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
|
||||||
|
|
||||||
# Set up initial state
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
handler.last_backpressure_event_time = 0
|
|
||||||
handler._resource_usage_state = RESOURCE_ACTIVE # Start with ACTIVE state
|
|
||||||
handler._resource_usage_changed = 0
|
|
||||||
|
|
||||||
with patch("time.time") as mock_time:
|
|
||||||
mock_time.return_value = 100
|
|
||||||
# Call with resource usage above OVERLOAD threshold (90% of maximum)
|
|
||||||
await handler.check_resource_availability(95, 100)
|
|
||||||
|
|
||||||
# Debug output
|
|
||||||
print(f"Resource usage state: {handler._resource_usage_state}")
|
|
||||||
print(f"Last backpressure event: {handler.last_backpressure_event}")
|
|
||||||
print(f"Last backpressure event time: {handler.last_backpressure_event_time}")
|
|
||||||
print(f"Time window: {mock_config.backpressure.time_window}")
|
|
||||||
print("Resource usage: 95, Maximum: 100")
|
|
||||||
print(f"Overload threshold calculation: {round(0.9 * 100)}")
|
|
||||||
|
|
||||||
# Verify OVERLOAD event was triggered
|
|
||||||
handler.handle_backpressure_overload_event.assert_called_once()
|
|
||||||
assert handler.last_backpressure_event == ScalingRequestAlert.OVERLOAD
|
|
||||||
assert handler.last_backpressure_event_time == 100
|
|
||||||
assert handler._resource_usage_state == RESOURCE_OVERLOAD
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_check_resource_availability_idle(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the check_resource_availability method when resource usage is in IDLE state.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler._handle_backpressure_idle_event = AsyncMock()
|
|
||||||
handler.handle_backpressure_overload_event = AsyncMock()
|
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
|
||||||
|
|
||||||
# Set up initial state
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.UPDATE
|
|
||||||
handler.last_backpressure_event_time = 0
|
|
||||||
handler._resource_usage_state = RESOURCE_UNSET # Start with UNSET state
|
|
||||||
handler._resource_usage_changed = 0
|
|
||||||
|
|
||||||
with patch("time.time") as mock_time:
|
|
||||||
# First call to set state to IDLE
|
|
||||||
mock_time.return_value = 50
|
|
||||||
await handler.check_resource_availability(10, 100)
|
|
||||||
|
|
||||||
# Second call after idle_duration has passed
|
|
||||||
mock_time.return_value = 50 + mock_config.backpressure.idle_duration + 1
|
|
||||||
await handler.check_resource_availability(10, 100)
|
|
||||||
|
|
||||||
# Verify IDLE event was triggered
|
|
||||||
handler._handle_backpressure_idle_event.assert_called_once()
|
|
||||||
assert handler.last_backpressure_event == ScalingRequestAlert.IDLE
|
|
||||||
assert (
|
|
||||||
handler.last_backpressure_event_time == 50 + mock_config.backpressure.idle_duration + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_check_resource_availability_update(self, mock_channel, mock_loop, mock_config):
|
|
||||||
"""
|
|
||||||
Test the check_resource_availability method when resource usage is in ACTIVE state.
|
|
||||||
"""
|
|
||||||
handler = BackpressureHandler(mock_channel, mock_loop, mock_config)
|
|
||||||
handler.publish_backpressure_request = AsyncMock()
|
|
||||||
handler.handle_backpressure_overload_event = AsyncMock()
|
|
||||||
handler._handle_backpressure_idle_event = AsyncMock()
|
|
||||||
|
|
||||||
# Set up initial state
|
|
||||||
handler.last_backpressure_event = ScalingRequestAlert.IDLE
|
|
||||||
handler.last_backpressure_event_time = 0
|
|
||||||
|
|
||||||
with patch("time.time") as mock_time:
|
|
||||||
mock_time.return_value = 100
|
|
||||||
# Call with resource usage between thresholds
|
|
||||||
await handler.check_resource_availability(50, 100)
|
|
||||||
|
|
||||||
# Verify UPDATE event was triggered
|
|
||||||
assert handler.publish_backpressure_request.call_count == 1
|
|
||||||
assert handler.last_backpressure_event == ScalingRequestAlert.UPDATE
|
|
||||||
assert handler.last_backpressure_event_time == 100
|
|
||||||
assert handler._resource_usage_state == RESOURCE_ACTIVE
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_publish_backpressure_request_exchange_exists(
|
async def test_publish_backpressure_request_exchange_exists(
|
||||||
@@ -571,7 +352,7 @@ class TestBackpressureHandler:
|
|||||||
mock_async_run.return_value = asyncio.Future()
|
mock_async_run.return_value = asyncio.Future()
|
||||||
mock_async_run.return_value.set_result(None)
|
mock_async_run.return_value.set_result(None)
|
||||||
await handler.publish_backpressure_request(
|
await handler.publish_backpressure_request(
|
||||||
ScaleRequestV1("test_service", "test_task", 100, 50, ScalingRequestAlert.UPDATE)
|
ScaleRequestV1("test_service", "test_task", 50)
|
||||||
)
|
)
|
||||||
assert mock_async_run.call_count == 1
|
assert mock_async_run.call_count == 1
|
||||||
# handler.exchange.publish.assert_called_once()
|
# handler.exchange.publish.assert_called_once()
|
||||||
@@ -595,7 +376,7 @@ class TestBackpressureHandler:
|
|||||||
mock_exchange
|
mock_exchange
|
||||||
) # Make it return the mock exchange
|
) # Make it return the mock exchange
|
||||||
await handler.publish_backpressure_request(
|
await handler.publish_backpressure_request(
|
||||||
ScaleRequestV1("test_service", "test_task", 100, 50, ScalingRequestAlert.UPDATE)
|
ScaleRequestV1("test_service", "test_task", 50)
|
||||||
)
|
)
|
||||||
assert mock_async_run.call_count == 2
|
assert mock_async_run.call_count == 2
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from amqp.model.model import (
|
|||||||
DataMessage,
|
DataMessage,
|
||||||
DataResponse,
|
DataResponse,
|
||||||
ScalingRequest,
|
ScalingRequest,
|
||||||
ScalingRequestAlert,
|
|
||||||
)
|
)
|
||||||
from amqp.model.snowflake_id import SnowflakeId
|
from amqp.model.snowflake_id import SnowflakeId
|
||||||
|
|
||||||
@@ -159,9 +158,7 @@ class TestScalingRequest(unittest.TestCase):
|
|||||||
self.request = ScalingRequest(
|
self.request = ScalingRequest(
|
||||||
service_id="test_service",
|
service_id="test_service",
|
||||||
task_id="test_task",
|
task_id="test_task",
|
||||||
max_availability=5,
|
|
||||||
current_availability=3,
|
current_availability=3,
|
||||||
request_type=ScalingRequestAlert.OVERLOAD,
|
|
||||||
version=1,
|
version=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,9 +168,7 @@ class TestScalingRequest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(data["serviceId"], "test_service")
|
self.assertEqual(data["serviceId"], "test_service")
|
||||||
self.assertEqual(data["taskId"], "test_task")
|
self.assertEqual(data["taskId"], "test_task")
|
||||||
self.assertEqual(data["maxAvailability"], 5)
|
|
||||||
self.assertEqual(data["currentAvailability"], 3)
|
self.assertEqual(data["currentAvailability"], 3)
|
||||||
self.assertEqual(data["requestType"], "OVERLOAD")
|
|
||||||
self.assertEqual(data["version"], 1)
|
self.assertEqual(data["version"], 1)
|
||||||
|
|
||||||
def test_from_json(self):
|
def test_from_json(self):
|
||||||
@@ -181,9 +176,7 @@ class TestScalingRequest(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"serviceId": "test_service",
|
"serviceId": "test_service",
|
||||||
"taskId": "test_task",
|
"taskId": "test_task",
|
||||||
"maxAvailability": 5,
|
|
||||||
"currentAvailability": 3,
|
"currentAvailability": 3,
|
||||||
"requestType": "OVERLOAD",
|
|
||||||
"version": 1,
|
"version": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -192,9 +185,7 @@ class TestScalingRequest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(request.service_id, "test_service")
|
self.assertEqual(request.service_id, "test_service")
|
||||||
self.assertEqual(request.task_id, "test_task")
|
self.assertEqual(request.task_id, "test_task")
|
||||||
self.assertEqual(request.max_availability, 5)
|
|
||||||
self.assertEqual(request.current_availability, 3)
|
self.assertEqual(request.current_availability, 3)
|
||||||
self.assertEqual(request.request_type, ScalingRequestAlert.OVERLOAD)
|
|
||||||
self.assertEqual(request.version, 1)
|
self.assertEqual(request.version, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
from amqp.config.amq_configuration import AMQAdapter, AMQConfiguration
|
from amqp.config.amq_configuration import AMQAdapter, AMQConfiguration
|
||||||
from amqp.model.model import DataMessage
|
|
||||||
from amqp.service.amq_service import AMQService
|
from amqp.service.amq_service import AMQService
|
||||||
|
|
||||||
|
|
||||||
@@ -146,21 +145,21 @@ class TestAMQService(unittest.IsolatedAsyncioTestCase):
|
|||||||
# Verify that client was reinitialized
|
# Verify that client was reinitialized
|
||||||
mock_client.init.assert_called_once()
|
mock_client.init.assert_called_once()
|
||||||
|
|
||||||
def test_send_message(self):
|
# def test_send_message(self):
|
||||||
# Create mock message and future
|
# # Create mock message and future
|
||||||
mock_message = Mock(spec=DataMessage)
|
# mock_message = Mock(spec=DataMessage)
|
||||||
mock_message.id.return_value = "test-id"
|
# mock_message.id.return_value = "test-id"
|
||||||
mock_future = MagicMock()
|
# mock_future = MagicMock()
|
||||||
|
#
|
||||||
# Setup mock message handler
|
# # Setup mock message handler
|
||||||
self.service.amq_data_message_handler = Mock()
|
# self.service.amq_data_message_handler = Mock()
|
||||||
self.service.amq_data_message_handler.outstanding = {}
|
# self.service.amq_data_message_handler.outstanding = {}
|
||||||
|
#
|
||||||
# Send message
|
# # Send message
|
||||||
self.service.send_message(mock_message, mock_future)
|
# self.service.send_message(mock_message, mock_future)
|
||||||
|
#
|
||||||
# Verify message was added to outstanding
|
# # Verify message was added to outstanding
|
||||||
self.assertEqual(self.service.amq_data_message_handler.outstanding["test-id"], mock_future)
|
# self.assertEqual(self.service.amq_data_message_handler.outstanding["test-id"], mock_future)
|
||||||
|
|
||||||
def test_remove_outstanding_future(self):
|
def test_remove_outstanding_future(self):
|
||||||
# Setup mock message handler with outstanding message
|
# Setup mock message handler with outstanding message
|
||||||
|
|||||||
Reference in New Issue
Block a user