vellum-workflow-server 1.7.2__tar.gz → 1.7.2.post2__tar.gz

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.

Files changed (33) hide show
  1. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/PKG-INFO +1 -1
  2. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/pyproject.toml +1 -1
  3. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +59 -0
  4. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/workflow_view.py +3 -4
  5. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/code_exec_runner.py +1 -0
  6. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/cancel_workflow.py +7 -5
  7. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/executor.py +10 -15
  8. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/utils.py +5 -1
  9. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/README.md +0 -0
  10. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/__init__.py +0 -0
  11. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/__init__.py +0 -0
  12. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/auth_middleware.py +0 -0
  13. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/healthz_view.py +0 -0
  14. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/tests/__init__.py +0 -0
  15. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/tests/test_input_display_mapping.py +0 -0
  16. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/api/tests/test_workflow_view.py +0 -0
  17. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/config.py +0 -0
  18. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/__init__.py +0 -0
  19. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/events.py +0 -0
  20. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/core/workflow_executor_context.py +0 -0
  21. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/server.py +0 -0
  22. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/start.py +0 -0
  23. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/__init__.py +0 -0
  24. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/exit_handler.py +0 -0
  25. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/log_proxy.py +0 -0
  26. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/oom_killer.py +0 -0
  27. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/sentry.py +0 -0
  28. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/system_utils.py +0 -0
  29. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/tests/__init__.py +0 -0
  30. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/tests/test_sentry_integration.py +0 -0
  31. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/tests/test_system_utils.py +0 -0
  32. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/tests/test_utils.py +0 -0
  33. {vellum_workflow_server-1.7.2 → vellum_workflow_server-1.7.2.post2}/src/workflow_server/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 1.7.2
3
+ Version: 1.7.2.post2
4
4
  Summary:
5
5
  License: AGPL
6
6
  Requires-Python: >=3.9.0,<4
@@ -3,7 +3,7 @@ name = "vellum-workflow-server"
3
3
 
4
4
  [tool.poetry]
5
5
  name = "vellum-workflow-server"
6
- version = "1.7.2"
6
+ version = "1.7.2.post2"
7
7
  description = ""
8
8
  readme = "README.md"
9
9
  authors = []
@@ -571,6 +571,65 @@ class BasicCancellableWorkflow(BaseWorkflow):
571
571
  )
572
572
 
573
573
 
574
+ def test_stream_workflow_route__timeout_emits_rejection_events():
575
+ """
576
+ Tests that when a workflow times out, we emit node and workflow rejection events.
577
+ """
578
+
579
+ span_id = uuid4()
580
+ request_body = {
581
+ "timeout": 1,
582
+ "execution_id": str(span_id),
583
+ "inputs": [],
584
+ "environment_api_key": "test",
585
+ "module": "workflow",
586
+ "files": {
587
+ "__init__.py": "",
588
+ "workflow.py": """\
589
+ import time
590
+
591
+ from vellum.workflows.nodes.bases.base import BaseNode
592
+ from vellum.workflows.workflows.base import BaseWorkflow
593
+
594
+
595
+ class LongRunningNode(BaseNode):
596
+ class Outputs(BaseNode.Outputs):
597
+ value: str
598
+
599
+ def run(self) -> Outputs:
600
+ time.sleep(30)
601
+ return self.Outputs(value="hello world")
602
+
603
+
604
+ class TimeoutWorkflow(BaseWorkflow):
605
+ graph = LongRunningNode
606
+ class Outputs(BaseWorkflow.Outputs):
607
+ final_value = LongRunningNode.Outputs.value
608
+
609
+ """,
610
+ },
611
+ }
612
+
613
+ status_code, events = flask_stream(request_body)
614
+
615
+ assert status_code == 200
616
+
617
+ event_names = [e["name"] for e in events]
618
+
619
+ assert "vembda.execution.initiated" in event_names
620
+ assert "workflow.execution.initiated" in event_names
621
+ assert "node.execution.initiated" in event_names
622
+
623
+ # TODO: Re-enable once solved SDK-side
624
+ # assert "node.execution.rejected" in event_names, "Should emit node.execution.rejected on timeout"
625
+
626
+ assert "workflow.execution.rejected" in event_names, "Should emit workflow.execution.rejected on timeout"
627
+
628
+ assert "vembda.execution.fulfilled" in event_names
629
+ vembda_fulfilled = next(e for e in events if e["name"] == "vembda.execution.fulfilled")
630
+ assert vembda_fulfilled["body"]["timed_out"] is True
631
+
632
+
574
633
  def test_stream_workflow_route__very_large_events(both_stream_types):
575
634
  # GIVEN a valid request body
576
635
  span_id = uuid4()
@@ -3,12 +3,11 @@ import importlib
3
3
  import inspect
4
4
  import json
5
5
  import logging
6
- from multiprocessing import Process, Queue, set_start_method
6
+ from multiprocessing import Event as MultiprocessingEvent, Process, Queue, set_start_method
7
7
  import os
8
8
  import pkgutil
9
9
  from queue import Empty
10
10
  import sys
11
- from threading import Event as ThreadingEvent
12
11
  import time
13
12
  import traceback
14
13
  from uuid import uuid4
@@ -123,8 +122,8 @@ def stream_workflow_route() -> Response:
123
122
  headers=headers,
124
123
  )
125
124
 
126
- cancel_signal = ThreadingEvent()
127
- timeout_signal = ThreadingEvent()
125
+ cancel_signal = MultiprocessingEvent()
126
+ timeout_signal = MultiprocessingEvent()
128
127
 
129
128
  process: Optional[Process] = None
130
129
  if ENABLE_PROCESS_WRAPPER:
@@ -50,6 +50,7 @@ def run_code_exec_stream() -> None:
50
50
  disable_redirect=True,
51
51
  # Timeouts are handled at the code exec level right now so just passing in an unused threading event
52
52
  timeout_signal=ThreadingEvent(),
53
+ cancel_signal=ThreadingEvent(),
53
54
  )
54
55
  for line in stream_iterator:
55
56
  print(f"{_EVENT_LINE}{json.dumps(line)}") # noqa: T201
@@ -1,11 +1,13 @@
1
1
  import logging
2
- from threading import Event, Event as ThreadingEvent, Thread
2
+ from threading import Thread
3
3
  import time
4
4
  from uuid import UUID
5
5
  from typing import Optional
6
6
 
7
7
  import requests
8
8
 
9
+ from workflow_server.core.utils import CancelSignal
10
+
9
11
  _TIMER_INTERVAL = 5
10
12
 
11
13
  logger = logging.getLogger(__name__)
@@ -23,18 +25,18 @@ def get_is_workflow_cancelled(execution_id: UUID, vembda_public_url: Optional[st
23
25
 
24
26
 
25
27
  class CancelWorkflowWatcherThread(Thread):
26
- _kill_switch: Event
28
+ _kill_switch: CancelSignal
27
29
  _execution_id: UUID
28
- _cancel_signal: ThreadingEvent
30
+ _cancel_signal: CancelSignal
29
31
  _vembda_public_url: Optional[str]
30
32
  _start = time.time()
31
33
 
32
34
  def __init__(
33
35
  self,
34
- kill_switch: Event,
36
+ kill_switch: CancelSignal,
35
37
  execution_id: UUID,
36
38
  vembda_public_url: Optional[str],
37
- cancel_signal: ThreadingEvent,
39
+ cancel_signal: CancelSignal,
38
40
  timeout_seconds: int,
39
41
  ) -> None:
40
42
  Thread.__init__(self)
@@ -8,7 +8,6 @@ import os
8
8
  import random
9
9
  import string
10
10
  import sys
11
- import threading
12
11
  from threading import Event as ThreadingEvent
13
12
  import time
14
13
  from traceback import format_exc
@@ -43,6 +42,7 @@ from workflow_server.core.events import (
43
42
  VembdaExecutionFulfilledEvent,
44
43
  )
45
44
  from workflow_server.core.utils import (
45
+ CancelSignal,
46
46
  create_vembda_rejected_event,
47
47
  is_events_emitting_enabled,
48
48
  serialize_vembda_rejected_event,
@@ -87,8 +87,8 @@ def _stream_node_wrapper(executor_context: NodeExecutorContext, queue: Queue) ->
87
87
  def _stream_workflow_wrapper(
88
88
  executor_context: WorkflowExecutorContext,
89
89
  queue: Queue,
90
- cancel_signal: Optional[ThreadingEvent],
91
- timeout_signal: ThreadingEvent,
90
+ cancel_signal: CancelSignal,
91
+ timeout_signal: CancelSignal,
92
92
  ) -> None:
93
93
  span_id_emitted = False
94
94
  try:
@@ -119,8 +119,8 @@ def _stream_workflow_wrapper(
119
119
  def stream_workflow_process_timeout(
120
120
  executor_context: WorkflowExecutorContext,
121
121
  queue: Queue,
122
- cancel_signal: ThreadingEvent,
123
- timeout_signal: ThreadingEvent,
122
+ cancel_signal: CancelSignal,
123
+ timeout_signal: CancelSignal,
124
124
  ) -> Process:
125
125
  workflow_process = Process(
126
126
  target=_stream_workflow_wrapper,
@@ -141,9 +141,9 @@ def stream_workflow_process_timeout(
141
141
 
142
142
  def stream_workflow(
143
143
  executor_context: WorkflowExecutorContext,
144
- timeout_signal: ThreadingEvent,
144
+ timeout_signal: CancelSignal,
145
+ cancel_signal: CancelSignal,
145
146
  disable_redirect: bool = True,
146
- cancel_signal: Optional[ThreadingEvent] = None,
147
147
  ) -> tuple[Iterator[dict], UUID]:
148
148
  cancel_watcher_kill_switch = ThreadingEvent()
149
149
  try:
@@ -172,7 +172,8 @@ def stream_workflow(
172
172
  state=workflow_state,
173
173
  node_output_mocks=node_output_mocks,
174
174
  event_filter=all_workflow_event_filter,
175
- cancel_signal=cancel_signal,
175
+ # TODO: Fix type in SDK
176
+ cancel_signal=cancel_signal, # type: ignore[arg-type]
176
177
  entrypoint_nodes=[run_from_node] if run_from_node else None,
177
178
  previous_execution_id=executor_context.previous_execution_id,
178
179
  )
@@ -235,12 +236,6 @@ def stream_workflow(
235
236
  finally:
236
237
  cancel_watcher_kill_switch.set()
237
238
 
238
- emitter_thread = next(
239
- (t for t in threading.enumerate() if t.name.endswith(".background_thread") and t.is_alive()), None
240
- )
241
- if emitter_thread:
242
- emitter_thread.join()
243
-
244
239
  workflow.join()
245
240
 
246
241
  return (
@@ -298,7 +293,7 @@ def stream_node(
298
293
  def _call_stream(
299
294
  executor_context: BaseExecutorContext,
300
295
  stream_generator: Callable[[], Generator[dict[str, Any], Any, None]],
301
- timeout_signal: ThreadingEvent,
296
+ timeout_signal: CancelSignal,
302
297
  disable_redirect: bool = True,
303
298
  ) -> Iterator[dict]:
304
299
  log_redirect: Optional[StringIO] = None
@@ -1,10 +1,14 @@
1
1
  from datetime import datetime
2
+ from multiprocessing.synchronize import Event as MultiprocessingEvent
3
+ from threading import Event as ThreadingEvent
2
4
  from uuid import uuid4
3
- from typing import Optional
5
+ from typing import Optional, Union
4
6
 
5
7
  from workflow_server.core.events import VembdaExecutionFulfilledBody, VembdaExecutionFulfilledEvent
6
8
  from workflow_server.core.workflow_executor_context import BaseExecutorContext
7
9
 
10
+ CancelSignal = Union[MultiprocessingEvent, ThreadingEvent]
11
+
8
12
 
9
13
  def _create_vembda_rejected_event_base(
10
14
  executor_context: Optional[BaseExecutorContext], error_message: str, timed_out: bool