vellum-workflow-server 0.14.70.post130__py3-none-any.whl → 0.14.70.post131__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vellum-workflow-server might be problematic. Click here for more details.
- {vellum_workflow_server-0.14.70.post130.dist-info → vellum_workflow_server-0.14.70.post131.dist-info}/METADATA +1 -1
- {vellum_workflow_server-0.14.70.post130.dist-info → vellum_workflow_server-0.14.70.post131.dist-info}/RECORD +10 -8
- workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +68 -0
- workflow_server/api/workflow_view.py +57 -2
- workflow_server/config.py +1 -0
- workflow_server/utils/oom_killer.py +10 -33
- workflow_server/utils/system_utils.py +74 -0
- workflow_server/utils/tests/test_system_utils.py +114 -0
- {vellum_workflow_server-0.14.70.post130.dist-info → vellum_workflow_server-0.14.70.post131.dist-info}/WHEEL +0 -0
- {vellum_workflow_server-0.14.70.post130.dist-info → vellum_workflow_server-0.14.70.post131.dist-info}/entry_points.txt +0 -0
|
@@ -5,10 +5,10 @@ workflow_server/api/healthz_view.py,sha256=itiRvBDBXncrw8Kbbc73UZLwqMAhgHOR3uSre
|
|
|
5
5
|
workflow_server/api/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
workflow_server/api/tests/test_input_display_mapping.py,sha256=drBZqMudFyB5wgiUOcMgRXz7E7ge-Qgxbstw4E4f0zE,2211
|
|
7
7
|
workflow_server/api/tests/test_workflow_view.py,sha256=wlVFBmKcoI-RdzfGPioeW46k6zaXyUeIerPc6m4aQls,7150
|
|
8
|
-
workflow_server/api/tests/test_workflow_view_stream_workflow_route.py,sha256=
|
|
9
|
-
workflow_server/api/workflow_view.py,sha256=
|
|
8
|
+
workflow_server/api/tests/test_workflow_view_stream_workflow_route.py,sha256=2gro4GD3FBuaA8T2-0oQxOOXh6zTf6hwxKb9CGU3x8g,24813
|
|
9
|
+
workflow_server/api/workflow_view.py,sha256=zuc2wm3y_F3zIcyP2HXsJKiaGpE2YvQZNm-ea6rZSeE,16205
|
|
10
10
|
workflow_server/code_exec_runner.py,sha256=tfijklTVkX4y45jeFTfrY2hVhdwo0VrLFc3SMeIiVYs,3096
|
|
11
|
-
workflow_server/config.py,sha256=
|
|
11
|
+
workflow_server/config.py,sha256=K5Tavm7wiqCZt0RWWue7zzb8N6e8aWnFOTNlBqEJPcI,1330
|
|
12
12
|
workflow_server/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
workflow_server/core/cancel_workflow.py,sha256=Ffkc3mzmrdMEUcD-sHfEhX4IwVrka-E--SxKA1dUfIU,2185
|
|
14
14
|
workflow_server/core/events.py,sha256=iscGJv8bS7WGEYR-ODnALIANuHpwOs2TdKzqDPrCOh0,1370
|
|
@@ -19,12 +19,14 @@ workflow_server/start.py,sha256=DgtQhuCLc07BIWyJPLPZKZsQ8jwEFsvvfIo7MdwVrpw,1998
|
|
|
19
19
|
workflow_server/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
workflow_server/utils/exit_handler.py,sha256=_FacDVi4zc3bfTA3D2mJsISePlJ8jpLrnGVo5-xZQFs,743
|
|
21
21
|
workflow_server/utils/log_proxy.py,sha256=nugi6fOgAYKX2X9DIc39TG366rsmmDUPoEtG3gzma_Y,3088
|
|
22
|
-
workflow_server/utils/oom_killer.py,sha256=
|
|
22
|
+
workflow_server/utils/oom_killer.py,sha256=4Sag_iRQWqbp62iIBn6nKP-pxUHguOF93DdVXZTtJDk,2809
|
|
23
23
|
workflow_server/utils/sentry.py,sha256=Pr3xKvHdk0XFSpXgy-55bWI4J3bbf_36gjDyLOs7oVU,855
|
|
24
|
+
workflow_server/utils/system_utils.py,sha256=fTzbdpmZ-0bXiNBLYYQdNJWtFAItZgIH8cLJdoXDuQQ,2114
|
|
24
25
|
workflow_server/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
workflow_server/utils/tests/test_system_utils.py,sha256=MdBxI9gxUOpR_JBAHpEz6dGFY6JjxhMSM2oExpqFvNA,4314
|
|
25
27
|
workflow_server/utils/tests/test_utils.py,sha256=qwK5Rmy3RQyjtlUrYAuGuDlBeRzZKsf1yS-y2IpUizQ,6452
|
|
26
28
|
workflow_server/utils/utils.py,sha256=Wqqn-1l2ugkGgy5paWWdt0AVxAyPMQCYcnRSSOMjXlA,4355
|
|
27
|
-
vellum_workflow_server-0.14.70.
|
|
28
|
-
vellum_workflow_server-0.14.70.
|
|
29
|
-
vellum_workflow_server-0.14.70.
|
|
30
|
-
vellum_workflow_server-0.14.70.
|
|
29
|
+
vellum_workflow_server-0.14.70.post131.dist-info/METADATA,sha256=X61wAnql7REkFKkXFcOA24ElsigAS9MQE5JMAA3TgVY,2245
|
|
30
|
+
vellum_workflow_server-0.14.70.post131.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
31
|
+
vellum_workflow_server-0.14.70.post131.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
|
|
32
|
+
vellum_workflow_server-0.14.70.post131.dist-info/RECORD,,
|
|
@@ -745,6 +745,74 @@ class EndNodeDisplay(BaseNodeDisplay[EndNode]):
|
|
|
745
745
|
assert events[2]["body"]["inputs"] == {"fruit": "cherry"}
|
|
746
746
|
|
|
747
747
|
|
|
748
|
+
@mock.patch("workflow_server.api.workflow_view.wait_for_available_process")
|
|
749
|
+
def test_stream_workflow_route__concurrent_request_rate_exceeded(mock_wait_for_available_process):
|
|
750
|
+
# GIVEN a valid request body
|
|
751
|
+
span_id = uuid4()
|
|
752
|
+
request_body = {
|
|
753
|
+
"execution_id": str(span_id),
|
|
754
|
+
"inputs": [],
|
|
755
|
+
"workspace_api_key": "test",
|
|
756
|
+
"environment_api_key": "test",
|
|
757
|
+
"module": "workflow",
|
|
758
|
+
"timeout": 360,
|
|
759
|
+
"files": {
|
|
760
|
+
"__init__.py": "",
|
|
761
|
+
"workflow.py": """\
|
|
762
|
+
from vellum.workflows import BaseWorkflow
|
|
763
|
+
|
|
764
|
+
class Workflow(BaseWorkflow):
|
|
765
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
766
|
+
foo = "hello"
|
|
767
|
+
""",
|
|
768
|
+
},
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# AND wait_for_available_process returns False
|
|
772
|
+
mock_wait_for_available_process.return_value = False
|
|
773
|
+
|
|
774
|
+
# WHEN we call the stream route
|
|
775
|
+
status_code, events = flask_stream(request_body)
|
|
776
|
+
|
|
777
|
+
# THEN we get a 200 response
|
|
778
|
+
assert status_code == 200, events
|
|
779
|
+
|
|
780
|
+
# THEN we get the expected events
|
|
781
|
+
assert events[0] == {
|
|
782
|
+
"id": mock.ANY,
|
|
783
|
+
"trace_id": mock.ANY,
|
|
784
|
+
"span_id": str(span_id),
|
|
785
|
+
"timestamp": mock.ANY,
|
|
786
|
+
"api_version": "2024-10-25",
|
|
787
|
+
"parent": None,
|
|
788
|
+
"name": "vembda.execution.initiated",
|
|
789
|
+
"body": {
|
|
790
|
+
"sdk_version": version("vellum-ai"),
|
|
791
|
+
"server_version": "local",
|
|
792
|
+
},
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# AND we get a vembda.execution.fulfilled event with error
|
|
796
|
+
assert events[1] == {
|
|
797
|
+
"id": mock.ANY,
|
|
798
|
+
"trace_id": events[0]["trace_id"],
|
|
799
|
+
"span_id": str(span_id),
|
|
800
|
+
"timestamp": mock.ANY,
|
|
801
|
+
"api_version": "2024-10-25",
|
|
802
|
+
"parent": None,
|
|
803
|
+
"name": "vembda.execution.fulfilled",
|
|
804
|
+
"body": {
|
|
805
|
+
"log": "",
|
|
806
|
+
"exit_code": -1,
|
|
807
|
+
"stderr": "Workflow server concurrent request rate exceeded. Process count: 0",
|
|
808
|
+
"container_overhead_latency": mock.ANY,
|
|
809
|
+
"timed_out": False,
|
|
810
|
+
},
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
assert len(events) == 2
|
|
814
|
+
|
|
815
|
+
|
|
748
816
|
def test_stream_workflow_route__with_environment_variables(both_stream_types):
|
|
749
817
|
# GIVEN a valid request body with environment variables
|
|
750
818
|
span_id = uuid4()
|
|
@@ -32,7 +32,12 @@ from workflow_server.core.workflow_executor_context import (
|
|
|
32
32
|
NodeExecutorContext,
|
|
33
33
|
WorkflowExecutorContext,
|
|
34
34
|
)
|
|
35
|
-
from workflow_server.utils.oom_killer import
|
|
35
|
+
from workflow_server.utils.oom_killer import get_is_oom_killed
|
|
36
|
+
from workflow_server.utils.system_utils import (
|
|
37
|
+
get_active_process_count,
|
|
38
|
+
increment_process_count,
|
|
39
|
+
wait_for_available_process,
|
|
40
|
+
)
|
|
36
41
|
from workflow_server.utils.utils import convert_json_inputs_to_vellum, get_version
|
|
37
42
|
|
|
38
43
|
bp = Blueprint("exec", __name__)
|
|
@@ -61,7 +66,8 @@ def stream_workflow_route() -> Response:
|
|
|
61
66
|
)
|
|
62
67
|
|
|
63
68
|
logger.info(
|
|
64
|
-
f"Starting workflow stream, execution ID: {context.execution_id},
|
|
69
|
+
f"Starting workflow stream, execution ID: {context.execution_id}, "
|
|
70
|
+
f"process count: {get_active_process_count()}"
|
|
65
71
|
)
|
|
66
72
|
|
|
67
73
|
# Create this event up here so timestamps are fully from the start to account for any unknown overhead
|
|
@@ -76,6 +82,26 @@ def stream_workflow_route() -> Response:
|
|
|
76
82
|
|
|
77
83
|
process_output_queue: Queue[dict] = Queue()
|
|
78
84
|
|
|
85
|
+
# We can exceed the concurrency count currently with long running workflows due to a knative issue. So here
|
|
86
|
+
# if we detect a memory problem just exit us early
|
|
87
|
+
if not wait_for_available_process():
|
|
88
|
+
return Response(
|
|
89
|
+
stream_with_context(
|
|
90
|
+
startup_error_generator(
|
|
91
|
+
context=context,
|
|
92
|
+
message=f"Workflow server concurrent request rate exceeded. "
|
|
93
|
+
f"Process count: {get_active_process_count()}",
|
|
94
|
+
vembda_initiated_event=vembda_initiated_event,
|
|
95
|
+
)
|
|
96
|
+
),
|
|
97
|
+
status=200,
|
|
98
|
+
content_type='application/x-ndjson"',
|
|
99
|
+
headers={
|
|
100
|
+
"X-Vellum-SDK-Version": vembda_initiated_event.body.sdk_version,
|
|
101
|
+
"X-Vellum-Server-Version": vembda_initiated_event.body.server_version,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
79
105
|
try:
|
|
80
106
|
process = stream_workflow_process_timeout(
|
|
81
107
|
executor_context=context,
|
|
@@ -388,3 +414,32 @@ def get_node_request_context(data: dict) -> NodeExecutorContext:
|
|
|
388
414
|
}
|
|
389
415
|
|
|
390
416
|
return NodeExecutorContext.model_validate(context_data)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def startup_error_generator(
|
|
420
|
+
vembda_initiated_event: VembdaExecutionInitiatedEvent, message: str, context: WorkflowExecutorContext
|
|
421
|
+
) -> Generator[str, None, None]:
|
|
422
|
+
try:
|
|
423
|
+
yield "\n"
|
|
424
|
+
yield vembda_initiated_event.model_dump_json()
|
|
425
|
+
yield "\n"
|
|
426
|
+
yield VembdaExecutionFulfilledEvent(
|
|
427
|
+
id=uuid4(),
|
|
428
|
+
timestamp=datetime.now(),
|
|
429
|
+
trace_id=context.trace_id,
|
|
430
|
+
span_id=context.execution_id,
|
|
431
|
+
body=VembdaExecutionFulfilledBody(
|
|
432
|
+
exit_code=-1,
|
|
433
|
+
container_overhead_latency=context.container_overhead_latency,
|
|
434
|
+
stderr=message,
|
|
435
|
+
),
|
|
436
|
+
parent=None,
|
|
437
|
+
).model_dump_json()
|
|
438
|
+
yield "\n"
|
|
439
|
+
yield "END"
|
|
440
|
+
yield "\n"
|
|
441
|
+
|
|
442
|
+
logger.error("Workflow stream could not start from resource constraints")
|
|
443
|
+
except GeneratorExit:
|
|
444
|
+
app.logger.error("Client disconnected in the middle of the stream")
|
|
445
|
+
return
|
workflow_server/config.py
CHANGED
|
@@ -27,6 +27,7 @@ MEMORY_LIMIT_MB = int(os.getenv("MEMORY_LIMIT_MB", "2048"))
|
|
|
27
27
|
PORT = os.getenv("PORT", "8000")
|
|
28
28
|
VELLUM_API_URL_HOST = os.getenv("VELLUM_API_URL_HOST", "localhost")
|
|
29
29
|
VELLUM_API_URL_PORT = os.getenv("VELLUM_API_URL_PORT", 8000)
|
|
30
|
+
CONCURRENCY = int(os.getenv("CONCURRENCY", "8"))
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def is_development() -> bool:
|
|
@@ -11,37 +11,22 @@ from time import sleep
|
|
|
11
11
|
|
|
12
12
|
from workflow_server.config import MEMORY_LIMIT_MB
|
|
13
13
|
from workflow_server.utils.exit_handler import process_killed_switch
|
|
14
|
+
from workflow_server.utils.system_utils import (
|
|
15
|
+
FORCE_GC_MEMORY_PERCENT,
|
|
16
|
+
WARN_MEMORY_PERCENT,
|
|
17
|
+
get_active_process_count,
|
|
18
|
+
get_memory_in_use_mb,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
logger = logging.getLogger(__name__)
|
|
16
22
|
|
|
17
23
|
_oom_killed_switch = multiprocessing.Event()
|
|
18
24
|
|
|
19
25
|
_MAX_MEMORY_PERCENT = 0.97
|
|
20
|
-
_WARN_MEMORY_PERCENT = 0.90
|
|
21
|
-
_FORCE_GC_MEMORY_PERCENT = 0.75
|
|
22
26
|
_FORCE_COLLECT_MEMORY_PERCENT = 0.90
|
|
23
|
-
_ACTIVE_PROCESS_COUNT = multiprocessing.Value("i", 0)
|
|
24
|
-
_ACTIVE_PROCESS_LOCK = multiprocessing.Lock()
|
|
25
27
|
_KILL_GRACE_PERIOD = 5
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
def increment_process_count(change: int) -> None:
|
|
29
|
-
result = _ACTIVE_PROCESS_LOCK.acquire(timeout=5)
|
|
30
|
-
try:
|
|
31
|
-
if result:
|
|
32
|
-
global _ACTIVE_PROCESS_COUNT
|
|
33
|
-
_ACTIVE_PROCESS_COUNT.value += change # type: ignore
|
|
34
|
-
else:
|
|
35
|
-
logger.error("Failed to lock workflow server process count global.")
|
|
36
|
-
finally:
|
|
37
|
-
if result:
|
|
38
|
-
_ACTIVE_PROCESS_LOCK.release()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def get_active_process_count() -> int:
|
|
42
|
-
return _ACTIVE_PROCESS_COUNT.value # type: ignore
|
|
43
|
-
|
|
44
|
-
|
|
45
30
|
def start_oom_killer_worker() -> None:
|
|
46
31
|
logger.info("Starting oom killer watcher...")
|
|
47
32
|
OomKillerThread(kill_switch=_oom_killed_switch).start()
|
|
@@ -79,19 +64,11 @@ class OomKillerThread(Thread):
|
|
|
79
64
|
if process_killed_switch.is_set():
|
|
80
65
|
exit(1)
|
|
81
66
|
sleep(1)
|
|
82
|
-
try:
|
|
83
|
-
with open("/sys/fs/cgroup/memory/memory.usage_in_bytes", "r") as file:
|
|
84
|
-
memory_bytes = file.read()
|
|
85
|
-
except Exception:
|
|
86
|
-
logger.error("Unable to get current memory.")
|
|
87
|
-
return
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
memory_mb = get_memory_in_use_mb()
|
|
69
|
+
if not memory_mb:
|
|
91
70
|
return
|
|
92
71
|
|
|
93
|
-
memory_mb = int(memory_bytes) / 1024 / 1024
|
|
94
|
-
|
|
95
72
|
if memory_mb > (MEMORY_LIMIT_MB * _MAX_MEMORY_PERCENT):
|
|
96
73
|
self._kill_switch.set()
|
|
97
74
|
logger.error(
|
|
@@ -103,13 +80,13 @@ class OomKillerThread(Thread):
|
|
|
103
80
|
os.kill(pid, signal.SIGKILL)
|
|
104
81
|
sys.exit(1)
|
|
105
82
|
|
|
106
|
-
if memory_mb > (MEMORY_LIMIT_MB *
|
|
83
|
+
if memory_mb > (MEMORY_LIMIT_MB * WARN_MEMORY_PERCENT):
|
|
107
84
|
logger.warning(
|
|
108
85
|
f"Memory usage exceeded 90% of limit, memory: {memory_mb}MB, "
|
|
109
86
|
f"Process Count: {get_active_process_count()}"
|
|
110
87
|
)
|
|
111
88
|
|
|
112
|
-
if memory_mb > (MEMORY_LIMIT_MB *
|
|
89
|
+
if memory_mb > (MEMORY_LIMIT_MB * FORCE_GC_MEMORY_PERCENT):
|
|
113
90
|
if time.time() - last_gc >= 20:
|
|
114
91
|
logger.info("Forcing garbage collect from memory pressure")
|
|
115
92
|
gc.collect()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from workflow_server.config import CONCURRENCY, MEMORY_LIMIT_MB
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
WARN_MEMORY_PERCENT = 0.90
|
|
12
|
+
FORCE_GC_MEMORY_PERCENT = 0.75
|
|
13
|
+
|
|
14
|
+
_MAX_PROCESS_COUNT = math.ceil(CONCURRENCY * 1.7)
|
|
15
|
+
_MEMORY_CHECK_INTERVAL_SECONDS = 3
|
|
16
|
+
_MAX_MEMORY_CHECK_ATTEMPTS = 5
|
|
17
|
+
_ACTIVE_PROCESS_COUNT = multiprocessing.Value("i", 0)
|
|
18
|
+
_ACTIVE_PROCESS_LOCK = multiprocessing.Lock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def increment_process_count(change: int) -> None:
|
|
22
|
+
result = _ACTIVE_PROCESS_LOCK.acquire(timeout=5)
|
|
23
|
+
try:
|
|
24
|
+
if result:
|
|
25
|
+
global _ACTIVE_PROCESS_COUNT
|
|
26
|
+
_ACTIVE_PROCESS_COUNT.value += change # type: ignore
|
|
27
|
+
else:
|
|
28
|
+
logger.error("Failed to lock workflow server process count global.")
|
|
29
|
+
finally:
|
|
30
|
+
if result:
|
|
31
|
+
_ACTIVE_PROCESS_LOCK.release()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_active_process_count() -> int:
|
|
35
|
+
return _ACTIVE_PROCESS_COUNT.value # type: ignore
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_memory_in_use_mb() -> Optional[float]:
|
|
39
|
+
try:
|
|
40
|
+
with open("/sys/fs/cgroup/memory/memory.usage_in_bytes", "r") as file:
|
|
41
|
+
memory_bytes = file.read()
|
|
42
|
+
except Exception:
|
|
43
|
+
logger.error("Unable to get current memory.")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if not memory_bytes:
|
|
47
|
+
logger.error("Unable to get current memory.")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return int(memory_bytes) / 1024 / 1024
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def wait_for_available_process() -> bool:
|
|
54
|
+
memory_loops = 0
|
|
55
|
+
process_available = False
|
|
56
|
+
|
|
57
|
+
while memory_loops < _MAX_MEMORY_CHECK_ATTEMPTS:
|
|
58
|
+
memory_mb = get_memory_in_use_mb()
|
|
59
|
+
|
|
60
|
+
exceeded_warn_limit = memory_mb and memory_mb > (MEMORY_LIMIT_MB * WARN_MEMORY_PERCENT)
|
|
61
|
+
exceeded_process_limit = (
|
|
62
|
+
get_active_process_count() > _MAX_PROCESS_COUNT
|
|
63
|
+
and memory_mb
|
|
64
|
+
and memory_mb > (MEMORY_LIMIT_MB * FORCE_GC_MEMORY_PERCENT)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not exceeded_process_limit and not exceeded_warn_limit:
|
|
68
|
+
process_available = True
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
memory_loops += 1
|
|
72
|
+
time.sleep(_MEMORY_CHECK_INTERVAL_SECONDS)
|
|
73
|
+
|
|
74
|
+
return process_available
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from unittest.mock import mock_open, patch
|
|
2
|
+
|
|
3
|
+
from workflow_server.config import MEMORY_LIMIT_MB
|
|
4
|
+
from workflow_server.utils.system_utils import (
|
|
5
|
+
FORCE_GC_MEMORY_PERCENT,
|
|
6
|
+
WARN_MEMORY_PERCENT,
|
|
7
|
+
get_memory_in_use_mb,
|
|
8
|
+
wait_for_available_process,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_get_memory_in_use_mb_success():
|
|
13
|
+
# Test with 1GB of memory (1024MB)
|
|
14
|
+
test_memory_bytes = "1073741824"
|
|
15
|
+
with patch("builtins.open", mock_open(read_data=test_memory_bytes)):
|
|
16
|
+
result = get_memory_in_use_mb()
|
|
17
|
+
|
|
18
|
+
assert result == 1024.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_memory_in_use_mb_empty_file():
|
|
22
|
+
with patch("builtins.open", mock_open(read_data="")):
|
|
23
|
+
result = get_memory_in_use_mb()
|
|
24
|
+
|
|
25
|
+
assert result is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_get_memory_in_use_mb_file_not_found():
|
|
29
|
+
with patch("builtins.open", side_effect=FileNotFoundError()):
|
|
30
|
+
result = get_memory_in_use_mb()
|
|
31
|
+
|
|
32
|
+
assert result is None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_get_memory_in_use_mb_zero_memory():
|
|
36
|
+
with patch("builtins.open", mock_open(read_data="0")):
|
|
37
|
+
result = get_memory_in_use_mb()
|
|
38
|
+
assert result == 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@patch("workflow_server.utils.system_utils.time.sleep")
|
|
42
|
+
@patch("workflow_server.utils.system_utils.get_memory_in_use_mb")
|
|
43
|
+
@patch("workflow_server.utils.system_utils.get_active_process_count")
|
|
44
|
+
def test_wait_for_available_process_immediate_availability(mock_get_active_process_count, mock_get_memory, mock_sleep):
|
|
45
|
+
# Mock memory usage below warning limit and process limit below
|
|
46
|
+
mock_get_memory.return_value = MEMORY_LIMIT_MB * (WARN_MEMORY_PERCENT - 0.1)
|
|
47
|
+
mock_get_active_process_count.return_value = 10
|
|
48
|
+
|
|
49
|
+
result = wait_for_available_process()
|
|
50
|
+
|
|
51
|
+
assert result is True
|
|
52
|
+
|
|
53
|
+
# Should not sleep if immediately available
|
|
54
|
+
mock_sleep.assert_not_called()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@patch("workflow_server.utils.system_utils.time.sleep")
|
|
58
|
+
@patch("workflow_server.utils.system_utils.get_memory_in_use_mb")
|
|
59
|
+
@patch("workflow_server.utils.system_utils.get_active_process_count")
|
|
60
|
+
def test_wait_for_available_process_becomes_available(mock_get_active_process_count, mock_get_memory, mock_sleep):
|
|
61
|
+
# First two calls indicate high memory usage, third call shows available memory
|
|
62
|
+
mock_get_memory.side_effect = [
|
|
63
|
+
MEMORY_LIMIT_MB * (WARN_MEMORY_PERCENT + 0.1),
|
|
64
|
+
MEMORY_LIMIT_MB * (WARN_MEMORY_PERCENT + 0.1),
|
|
65
|
+
MEMORY_LIMIT_MB * (WARN_MEMORY_PERCENT - 0.1),
|
|
66
|
+
]
|
|
67
|
+
mock_get_active_process_count.return_value = 10
|
|
68
|
+
|
|
69
|
+
result = wait_for_available_process()
|
|
70
|
+
|
|
71
|
+
assert result is True
|
|
72
|
+
# Should sleep twice before becoming available
|
|
73
|
+
assert mock_sleep.call_count == 2
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@patch("workflow_server.utils.system_utils.time.sleep")
|
|
77
|
+
@patch("workflow_server.utils.system_utils.get_memory_in_use_mb")
|
|
78
|
+
@patch("workflow_server.utils.system_utils.get_active_process_count")
|
|
79
|
+
def test_wait_for_available_process_never_available(mock_get_active_process_count, mock_get_memory, mock_sleep):
|
|
80
|
+
# Return false if process isn't available from high memory usage
|
|
81
|
+
mock_get_memory.return_value = MEMORY_LIMIT_MB * (WARN_MEMORY_PERCENT + 0.1)
|
|
82
|
+
mock_get_active_process_count.return_value = 13
|
|
83
|
+
|
|
84
|
+
result = wait_for_available_process()
|
|
85
|
+
assert result is False
|
|
86
|
+
|
|
87
|
+
# Should sleep for each attempt
|
|
88
|
+
assert mock_sleep.call_count == 5
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@patch("workflow_server.utils.system_utils.time.sleep")
|
|
92
|
+
@patch("workflow_server.utils.system_utils.get_memory_in_use_mb")
|
|
93
|
+
@patch("workflow_server.utils.system_utils.get_active_process_count")
|
|
94
|
+
def test_wait_for_available_process_memory_none(mock_get_active_process_count, mock_get_memory, mock_sleep):
|
|
95
|
+
# Test when memory reading fails that result is still true
|
|
96
|
+
mock_get_memory.return_value = None
|
|
97
|
+
mock_get_active_process_count.return_value = 10
|
|
98
|
+
|
|
99
|
+
result = wait_for_available_process()
|
|
100
|
+
assert result is True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@patch("workflow_server.utils.system_utils.time.sleep")
|
|
104
|
+
@patch("workflow_server.utils.system_utils.get_memory_in_use_mb")
|
|
105
|
+
@patch("workflow_server.utils.system_utils.get_active_process_count")
|
|
106
|
+
def test_wait_for_available_process_high_process_count_but_low_memory(
|
|
107
|
+
mock_get_active_process_count, mock_get_memory, mock_sleep
|
|
108
|
+
):
|
|
109
|
+
# Test when process count is high but memory is low
|
|
110
|
+
mock_get_memory.return_value = MEMORY_LIMIT_MB * (FORCE_GC_MEMORY_PERCENT - 0.1)
|
|
111
|
+
mock_get_active_process_count.return_value = 13
|
|
112
|
+
|
|
113
|
+
result = wait_for_available_process()
|
|
114
|
+
assert result is True
|
|
File without changes
|