feat/58 - improve Backpressure algorithm
This commit is contained in:
@@ -3,6 +3,7 @@ import json
|
||||
import time
|
||||
from asyncio import AbstractEventLoop
|
||||
from threading import Thread
|
||||
from typing import Optional
|
||||
|
||||
import psutil
|
||||
from aio_pika import Message
|
||||
@@ -94,9 +95,8 @@ class RunningAverage:
|
||||
|
||||
class BackpressureHandler:
|
||||
# Track the number of messages currently being processed
|
||||
current_parallel_executions = 0
|
||||
current_load = 0
|
||||
# helps detect IDLE condition
|
||||
last_data_message_time = 0
|
||||
# helps prevent flooding the system with backpressure events
|
||||
last_backpressure_event_time = 0
|
||||
last_backpressure_event = ScalingRequestAlert.UPDATE
|
||||
@@ -116,93 +116,74 @@ class BackpressureHandler:
|
||||
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
|
||||
self.maximum_load = self.config.backpressure.threshold_load
|
||||
self.average_cpu_usage = None
|
||||
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:
|
||||
"""
|
||||
Check if the loop should continue running.
|
||||
Positive value of do_loop indicates the number of iterations left.
|
||||
Negative value indicates the loop should run indefinitely.
|
||||
"""
|
||||
_val = self.do_loop != 0
|
||||
if self.do_loop > 0:
|
||||
self.do_loop -= 1
|
||||
return _val
|
||||
|
||||
def increase_parallel_executions(self):
|
||||
"""Increase the number of parallel executions"""
|
||||
def increase_current_load(self):
|
||||
"""Increase the number of parallel executions, or current load"""
|
||||
logging_info(
|
||||
"Backpressure: Increase parallel executions, current=%s",
|
||||
self.current_parallel_executions,
|
||||
self.current_load,
|
||||
)
|
||||
self.current_parallel_executions += 1
|
||||
self.current_load += 1
|
||||
|
||||
def decrease_parallel_executions(self):
|
||||
"""Decrease the number of parallel executions"""
|
||||
def decrease_current_load(self):
|
||||
"""Decrease the number of parallel executions, or current load"""
|
||||
logging_info(
|
||||
"Backpressure: Decrease parallel executions, current=%s",
|
||||
self.current_parallel_executions,
|
||||
self.current_load,
|
||||
)
|
||||
if self.current_parallel_executions > 0:
|
||||
self.current_parallel_executions -= 1
|
||||
if self.current_load > 0:
|
||||
self.current_load -= 1
|
||||
|
||||
def update_parallel_executions(self, count: int):
|
||||
"""Update the number of parallel executions"""
|
||||
self.current_parallel_executions = count
|
||||
def update_current_load(self, count: int):
|
||||
"""Update the number of parallel executions, or current load"""
|
||||
self.current_load = count
|
||||
|
||||
def update_last_data_message_time(self):
|
||||
logging_info("Backpressure: Update last data message time")
|
||||
# logging_info("Backpressure: Update last data message time")
|
||||
"""Update the last data message time"""
|
||||
self.last_data_message_time = time.time()
|
||||
self.last_backpressure_event_time = time.time()
|
||||
|
||||
async def update_backpressure_value(self, current: int, maximum: int):
|
||||
"""
|
||||
Update the current backpressure value and check for overload conditions.
|
||||
This method is called by the AMQService.backpressure() method to update
|
||||
the current parallel executions count and check for overload conditions.
|
||||
|
||||
|
||||
Args:
|
||||
current: Current value of the backpressure metric
|
||||
maximum: Maximum value of the backpressure metric
|
||||
"""
|
||||
logging_info(
|
||||
"Backpressure: Updating backpressure value, current=%s, maximum=%s",
|
||||
current,
|
||||
maximum
|
||||
"Backpressure: Updating backpressure value, current=%s, maximum=%s", current, maximum
|
||||
)
|
||||
|
||||
if maximum > 0 and maximum > self.maximum_load:
|
||||
self.maximum_load = maximum
|
||||
|
||||
# Update the current parallel executions count
|
||||
self.update_parallel_executions(current)
|
||||
|
||||
# Update the last data message time to indicate activity
|
||||
self.update_last_data_message_time()
|
||||
|
||||
self.update_current_load(current)
|
||||
|
||||
# Check for overload conditions
|
||||
await self.check_overload_condition()
|
||||
|
||||
|
||||
# Update the last data message time to indicate activity
|
||||
self.update_last_data_message_time()
|
||||
|
||||
# If maximum has changed, update the parallel workers threshold
|
||||
if maximum != self.parallel_workers and maximum > 0:
|
||||
if maximum != self.maximum_load and maximum > 0:
|
||||
logging_info(
|
||||
"Backpressure: Updating maximum parallel workers from %s to %s",
|
||||
self.parallel_workers,
|
||||
maximum
|
||||
self.maximum_load,
|
||||
maximum,
|
||||
)
|
||||
self.parallel_workers = maximum
|
||||
self.maximum_load = maximum
|
||||
|
||||
def start_backpressure_monitor(self) -> Thread:
|
||||
# Check if CPU monitoring is enabled in configuration
|
||||
cpu_monitoring_enabled = self.config.backpressure.cpu_monitoring_enabled
|
||||
|
||||
if not cpu_monitoring_enabled:
|
||||
logging_info("Backpressure CPU monitoring is disabled by configuration")
|
||||
return None
|
||||
|
||||
def start_backpressure_monitor(self) -> Optional[Thread]:
|
||||
# Start the Backpressure monitor loop
|
||||
self.thread = Thread(target=self.backpressure_monitor_loop)
|
||||
self.thread.daemon = True # This makes it a daemon thread
|
||||
@@ -215,50 +196,54 @@ class BackpressureHandler:
|
||||
or if the service has been idle for too long (IDLE).
|
||||
"""
|
||||
current_time = time.time()
|
||||
self.update_last_data_message_time()
|
||||
|
||||
last_event_delta = current_time - self.last_backpressure_event_time
|
||||
logging_info(
|
||||
"Backpressure: Check conditions, current=%s, max=%s, last_activity=%s",
|
||||
self.current_parallel_executions,
|
||||
self.parallel_workers,
|
||||
current_time - self.last_data_message_time,
|
||||
self.current_load,
|
||||
self.maximum_load,
|
||||
last_event_delta,
|
||||
)
|
||||
|
||||
|
||||
# Check for OVERLOAD condition
|
||||
if self.current_parallel_executions >= self.parallel_workers - 1:
|
||||
if self.current_load >= round(0.8 * self.maximum_load):
|
||||
# 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()
|
||||
# update / reset time-window so that the OVERLOAD is not sent too often
|
||||
self.last_backpressure_event_time = current_time
|
||||
self.last_backpressure_event = ScalingRequestAlert.OVERLOAD
|
||||
|
||||
|
||||
# Check for IDLE condition
|
||||
elif self.current_parallel_executions == 0:
|
||||
elif self.current_load == 0:
|
||||
idle_duration = self.config.backpressure.idle_duration
|
||||
# Check if service has been idle for longer than the configured duration
|
||||
if (current_time - self.last_data_message_time > idle_duration and
|
||||
(current_time - self.last_backpressure_event_time > self.config.backpressure.time_window or
|
||||
self.last_backpressure_event != ScalingRequestAlert.IDLE)):
|
||||
|
||||
if (
|
||||
last_event_delta > idle_duration
|
||||
and self.last_backpressure_event == ScalingRequestAlert.IDLE
|
||||
):
|
||||
|
||||
logging_info(
|
||||
"Backpressure: Service has been idle for %s seconds (threshold: %s seconds)",
|
||||
current_time - self.last_data_message_time,
|
||||
idle_duration
|
||||
last_event_delta,
|
||||
idle_duration,
|
||||
)
|
||||
|
||||
|
||||
# Trigger the idle event
|
||||
await self._handle_backpressure_idle_event()
|
||||
# update / reset time-window so that the IDLE is not sent too often
|
||||
self.last_backpressure_event_time = current_time
|
||||
else:
|
||||
# Don't send IDLE right away when the usage drops to zero / below threshold, but wait for a while
|
||||
self.last_backpressure_event = ScalingRequestAlert.IDLE
|
||||
|
||||
|
||||
# If neither OVERLOAD nor IDLE, and it's time for an update, send UPDATE event
|
||||
elif (current_time - self.last_backpressure_event_time > self.config.backpressure.time_window):
|
||||
elif last_event_delta > self.config.backpressure.time_window:
|
||||
_scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
||||
self.swarm_service_id,
|
||||
self.swarm_task_id,
|
||||
self.parallel_workers,
|
||||
self.parallel_workers - self.current_parallel_executions,
|
||||
self.maximum_load,
|
||||
self.maximum_load - self.current_load,
|
||||
ScalingRequestAlert.UPDATE,
|
||||
)
|
||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
||||
@@ -266,8 +251,12 @@ class BackpressureHandler:
|
||||
self.last_backpressure_event_time = current_time
|
||||
self.last_backpressure_event = ScalingRequestAlert.UPDATE
|
||||
|
||||
self.update_last_data_message_time()
|
||||
|
||||
async def _backpressure_monitor(self):
|
||||
"""Monitor the backpressure conditions and trigger events accordingly"""
|
||||
# Check if CPU monitoring is enabled in configuration
|
||||
cpu_monitoring_enabled = self.config.backpressure.cpu_monitoring_enabled
|
||||
_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
|
||||
@@ -281,73 +270,76 @@ class BackpressureHandler:
|
||||
_cpu_usage_state = CPU_ACTIVE
|
||||
_cpu_usage_changed = time.time()
|
||||
while self._do_loop():
|
||||
_current_cpu_usage = psutil.cpu_percent(interval=None)
|
||||
self.average_cpu_usage.add(_current_cpu_usage)
|
||||
_average_cpu_usage = self.average_cpu_usage.get_average()
|
||||
_now = time.time()
|
||||
# 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
|
||||
if cpu_monitoring_enabled:
|
||||
_current_cpu_usage = psutil.cpu_percent(interval=None)
|
||||
self.average_cpu_usage.add(_current_cpu_usage)
|
||||
_average_cpu_usage = self.average_cpu_usage.get_average()
|
||||
_now = time.time()
|
||||
# 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_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
|
||||
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,
|
||||
# Check if the current time exceeds the last backpressure event time
|
||||
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
|
||||
)
|
||||
# Address the message to any adapter capable of supporting BACKPRESSURE request
|
||||
await self.publish_backpressure_request(_scaling_request)
|
||||
):
|
||||
# Trigger the overload event
|
||||
await self.handle_backpressure_overload_event()
|
||||
self.last_backpressure_event_time = _now
|
||||
self.last_backpressure_event = ScalingRequestAlert.UPDATE
|
||||
self.last_backpressure_event = ScalingRequestAlert.OVERLOAD
|
||||
elif (
|
||||
_cpu_usage_state == CPU_IDLE
|
||||
and _now - _cpu_usage_changed > _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
|
||||
else:
|
||||
# If CPU monitoring is disabled, just check the current load
|
||||
await self.check_overload_condition()
|
||||
|
||||
await asyncio.sleep(_monitor_interval)
|
||||
|
||||
def backpressure_monitor_loop(self):
|
||||
@@ -360,14 +352,12 @@ class BackpressureHandler:
|
||||
scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
||||
self.swarm_service_id,
|
||||
self.swarm_task_id,
|
||||
self.parallel_workers,
|
||||
self.parallel_workers - self.current_parallel_executions,
|
||||
self.maximum_load,
|
||||
self.maximum_load - self.current_load,
|
||||
ScalingRequestAlert.OVERLOAD,
|
||||
)
|
||||
# Address the message to any management service instance capable of supporting BACKPRESSURE 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.")
|
||||
@@ -375,14 +365,12 @@ class BackpressureHandler:
|
||||
scaling_request: ScaleRequestV1 = ScaleRequestV1(
|
||||
self.swarm_service_id,
|
||||
self.swarm_task_id,
|
||||
self.parallel_workers,
|
||||
self.parallel_workers - self.current_parallel_executions,
|
||||
self.maximum_load,
|
||||
self.maximum_load - self.current_load,
|
||||
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):
|
||||
# Publish the backpressure request to the management service
|
||||
@@ -429,3 +417,14 @@ class BackpressureHandler:
|
||||
if BackpressureHandler._callback_list is not None:
|
||||
BackpressureHandler._callback_list["_wrap_rabbit_mq_api"] = 1
|
||||
await await_future(asyncio.run_coroutine_threadsafe(_wrap_rabbit_mq_api(), self.loop))
|
||||
|
||||
def _do_loop(self) -> bool:
|
||||
"""
|
||||
Helper function for unit tests to perform several loops only. Check if the loop should continue running.
|
||||
Positive value of do_loop indicates the number of iterations left.
|
||||
Negative value indicates the loop should run indefinitely.
|
||||
"""
|
||||
_val = self.do_loop != 0
|
||||
if self.do_loop > 0:
|
||||
self.do_loop -= 1
|
||||
return _val
|
||||
|
||||
@@ -150,7 +150,7 @@ class Backpressure:
|
||||
|
||||
:param config: config parser to read configuration values
|
||||
"""
|
||||
self.threshold_threads = config.getint(
|
||||
self.threshold_load = config.getint(
|
||||
"CleverMicro-AMQ",
|
||||
self.PREFIX + "threshold",
|
||||
fallback=int(os.getenv("PARALLEL_WORKERS", 1)),
|
||||
|
||||
@@ -124,10 +124,10 @@ class DataMessageHandler:
|
||||
:return: DataResponse
|
||||
"""
|
||||
# Increment the counter for parallel executions and check resource availability
|
||||
self.backpressure_handler.increase_parallel_executions()
|
||||
self.backpressure_handler.increase_current_load()
|
||||
await self.backpressure_handler.check_overload_condition()
|
||||
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_load}] of [{self.backpressure_handler.maximum_load}]"
|
||||
)
|
||||
if message.reply_to:
|
||||
self.reply_to = message.reply_to
|
||||
@@ -155,7 +155,7 @@ class DataMessageHandler:
|
||||
except Exception as e:
|
||||
logging_error(f"Request handler: {e}")
|
||||
|
||||
self.backpressure_handler.decrease_parallel_executions()
|
||||
self.backpressure_handler.decrease_current_load()
|
||||
await self.backpressure_handler.check_overload_condition()
|
||||
|
||||
async def reconstitute_data_message(
|
||||
@@ -208,12 +208,12 @@ class DataMessageHandler:
|
||||
# map_as_string(amq_message.trace_info()), context_with_span, context)
|
||||
|
||||
def record_duration(self, start, delivery_tag):
|
||||
global last_data_message_time
|
||||
last_data_message_time = time.time()
|
||||
global last_backpressure_event_time
|
||||
last_backpressure_event_time = time.time()
|
||||
logging_info(
|
||||
"######[%s] Done, single ACK: %sms.",
|
||||
delivery_tag,
|
||||
last_data_message_time - start,
|
||||
last_backpressure_event_time - start,
|
||||
)
|
||||
|
||||
async def send_reply(self, reply_to: str, response: DataResponse):
|
||||
|
||||
@@ -83,24 +83,27 @@ class AMQService:
|
||||
logging.info("***********************************************")
|
||||
self.loop.run_forever()
|
||||
|
||||
def backpressure(self, current: int, maximum: int = -1):
|
||||
def backpressure(self, current_availability: 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
|
||||
based on the current_availability and maximum values.
|
||||
- OVERLOAD event is triggered immediately when the current_availability 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
|
||||
- IDLE event is triggered when the current_availability 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'.
|
||||
:param current_availability: Currently available caopacity.
|
||||
:param maximum: Maximum value of the backpressure metric. If undefined or set to lt 0 (e.g. -1),
|
||||
the maximum is than hgdetermined from max value of current_availability seen over the time.
|
||||
"""
|
||||
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)
|
||||
maximum = self.amq_configuration.backpressure.threshold_load
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.backpressure_handler.update_backpressure_value(current_availability, maximum),
|
||||
loop=self.backpressure_handler.loop,
|
||||
)
|
||||
|
||||
def rpc(self, service_name: str, fq_method_name: str, **kwargs) -> Future:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user