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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 0.14.70.post130
3
+ Version: 0.14.70.post131
4
4
  Summary:
5
5
  License: AGPL
6
6
  Requires-Python: >=3.9.0,<4
@@ -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=5k2caHDaR2pBk-o8JPffnO38VLMB1f1dsSfvztlU_3Q,22826
9
- workflow_server/api/workflow_view.py,sha256=duiMnAZ7PRpoPz63s9z37pxUxGR-9yAi3qxG9APXCao,14244
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=Jk1kmncI7g2LTIujssIpncD_eQoIowL_5oARQ6XpkJ0,1281
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=8WB0nQWjmnjW9QzvNNwfYoBFB3yDHM3_OmnryeC8G3A,3657
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.post130.dist-info/METADATA,sha256=AvtHdGxUrP1n6e6wmVntOutz63X7FSKgIJvcAASmc1k,2245
28
- vellum_workflow_server-0.14.70.post130.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
29
- vellum_workflow_server-0.14.70.post130.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
30
- vellum_workflow_server-0.14.70.post130.dist-info/RECORD,,
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 get_active_process_count, get_is_oom_killed, increment_process_count
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}, process count: {get_active_process_count()}"
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
- if not memory_bytes:
90
- logger.error("Unable to get current memory.")
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 * _WARN_MEMORY_PERCENT):
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 * _FORCE_GC_MEMORY_PERCENT):
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