vellum-workflow-server 1.8.2__py3-none-any.whl → 1.10.7__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.
- {vellum_workflow_server-1.8.2.dist-info → vellum_workflow_server-1.10.7.dist-info}/METADATA +3 -3
- {vellum_workflow_server-1.8.2.dist-info → vellum_workflow_server-1.10.7.dist-info}/RECORD +17 -15
- workflow_server/api/auth_middleware.py +2 -2
- workflow_server/api/status_view.py +19 -0
- workflow_server/api/tests/test_workflow_view.py +75 -24
- workflow_server/api/tests/test_workflow_view_async_exec.py +410 -0
- workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +100 -1
- workflow_server/api/workflow_view.py +206 -93
- workflow_server/config.py +2 -0
- workflow_server/core/executor.py +47 -67
- workflow_server/core/utils.py +4 -0
- workflow_server/core/workflow_executor_context.py +18 -1
- workflow_server/server.py +2 -0
- workflow_server/start.py +8 -2
- workflow_server/utils/exit_handler.py +30 -1
- {vellum_workflow_server-1.8.2.dist-info → vellum_workflow_server-1.10.7.dist-info}/WHEEL +0 -0
- {vellum_workflow_server-1.8.2.dist-info → vellum_workflow_server-1.10.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from workflow_server.server import create_app
|
|
7
|
+
from workflow_server.utils.system_utils import get_active_process_count
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(autouse=True)
|
|
11
|
+
def drain_background_threads():
|
|
12
|
+
"""
|
|
13
|
+
Ensures background threads from previous tests complete before starting the next test.
|
|
14
|
+
This prevents cross-test interference in process count assertions.
|
|
15
|
+
"""
|
|
16
|
+
baseline = get_active_process_count()
|
|
17
|
+
yield
|
|
18
|
+
|
|
19
|
+
deadline = time.time() + 15
|
|
20
|
+
while time.time() < deadline:
|
|
21
|
+
current_count = get_active_process_count()
|
|
22
|
+
if current_count == baseline:
|
|
23
|
+
break
|
|
24
|
+
time.sleep(0.1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_async_exec_route__happy_path():
|
|
28
|
+
"""
|
|
29
|
+
Tests that the async-exec route successfully accepts a valid workflow and returns immediately.
|
|
30
|
+
"""
|
|
31
|
+
# GIVEN a Flask application
|
|
32
|
+
flask_app = create_app()
|
|
33
|
+
|
|
34
|
+
# AND a valid workflow request
|
|
35
|
+
span_id = uuid4()
|
|
36
|
+
request_body = {
|
|
37
|
+
"execution_id": str(span_id),
|
|
38
|
+
"inputs": [],
|
|
39
|
+
"environment_api_key": "test",
|
|
40
|
+
"module": "workflow",
|
|
41
|
+
"timeout": 360,
|
|
42
|
+
"files": {
|
|
43
|
+
"__init__.py": "",
|
|
44
|
+
"workflow.py": """\
|
|
45
|
+
from vellum.workflows import BaseWorkflow
|
|
46
|
+
|
|
47
|
+
class Workflow(BaseWorkflow):
|
|
48
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
49
|
+
foo = "hello"
|
|
50
|
+
""",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# WHEN we make a request to the async-exec route
|
|
55
|
+
with flask_app.test_client() as test_client:
|
|
56
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
57
|
+
|
|
58
|
+
# THEN we should get a 200 response
|
|
59
|
+
assert response.status_code == 200
|
|
60
|
+
|
|
61
|
+
# AND the response should indicate success
|
|
62
|
+
assert response.json == {"success": True}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_async_exec_route__with_inputs():
|
|
66
|
+
"""
|
|
67
|
+
Tests that the async-exec route handles workflows with inputs correctly.
|
|
68
|
+
"""
|
|
69
|
+
# GIVEN a Flask application
|
|
70
|
+
flask_app = create_app()
|
|
71
|
+
|
|
72
|
+
# AND a valid workflow request with inputs
|
|
73
|
+
span_id = uuid4()
|
|
74
|
+
request_body = {
|
|
75
|
+
"execution_id": str(span_id),
|
|
76
|
+
"inputs": [
|
|
77
|
+
{"name": "foo", "type": "STRING", "value": "hello"},
|
|
78
|
+
],
|
|
79
|
+
"environment_api_key": "test",
|
|
80
|
+
"module": "workflow",
|
|
81
|
+
"timeout": 360,
|
|
82
|
+
"files": {
|
|
83
|
+
"__init__.py": "",
|
|
84
|
+
"workflow.py": """\
|
|
85
|
+
from vellum.workflows import BaseWorkflow
|
|
86
|
+
from vellum.workflows.state import BaseState
|
|
87
|
+
from .inputs import Inputs
|
|
88
|
+
|
|
89
|
+
class Workflow(BaseWorkflow[Inputs, BaseState]):
|
|
90
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
91
|
+
foo = "hello"
|
|
92
|
+
""",
|
|
93
|
+
"inputs.py": """\
|
|
94
|
+
from vellum.workflows.inputs import BaseInputs
|
|
95
|
+
|
|
96
|
+
class Inputs(BaseInputs):
|
|
97
|
+
foo: str
|
|
98
|
+
""",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# WHEN we make a request to the async-exec route
|
|
103
|
+
with flask_app.test_client() as test_client:
|
|
104
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
105
|
+
|
|
106
|
+
# THEN we should get a 200 response
|
|
107
|
+
assert response.status_code == 200
|
|
108
|
+
|
|
109
|
+
# AND the response should indicate success
|
|
110
|
+
assert response.json == {"success": True}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_async_exec_route__with_state():
|
|
114
|
+
"""
|
|
115
|
+
Tests that the async-exec route handles workflows with state correctly.
|
|
116
|
+
"""
|
|
117
|
+
# GIVEN a Flask application
|
|
118
|
+
flask_app = create_app()
|
|
119
|
+
|
|
120
|
+
# AND a valid workflow request with state
|
|
121
|
+
span_id = uuid4()
|
|
122
|
+
request_body = {
|
|
123
|
+
"execution_id": str(span_id),
|
|
124
|
+
"state": {"foo": "bar"},
|
|
125
|
+
"environment_api_key": "test",
|
|
126
|
+
"module": "workflow",
|
|
127
|
+
"timeout": 360,
|
|
128
|
+
"files": {
|
|
129
|
+
"__init__.py": "",
|
|
130
|
+
"workflow.py": """\
|
|
131
|
+
from vellum.workflows import BaseWorkflow
|
|
132
|
+
from vellum.workflows.inputs import BaseInputs
|
|
133
|
+
from .state import State
|
|
134
|
+
|
|
135
|
+
class Workflow(BaseWorkflow[BaseInputs, State]):
|
|
136
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
137
|
+
foo = State.foo
|
|
138
|
+
""",
|
|
139
|
+
"state.py": """\
|
|
140
|
+
from vellum.workflows.state import BaseState
|
|
141
|
+
|
|
142
|
+
class State(BaseState):
|
|
143
|
+
foo: str
|
|
144
|
+
""",
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# WHEN we make a request to the async-exec route
|
|
149
|
+
with flask_app.test_client() as test_client:
|
|
150
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
151
|
+
|
|
152
|
+
# THEN we should get a 200 response
|
|
153
|
+
assert response.status_code == 200
|
|
154
|
+
|
|
155
|
+
# AND the response should indicate success
|
|
156
|
+
assert response.json == {"success": True}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_async_exec_route__invalid_context():
|
|
160
|
+
"""
|
|
161
|
+
Tests that the async-exec route returns 400 for invalid request context.
|
|
162
|
+
"""
|
|
163
|
+
# GIVEN a Flask application
|
|
164
|
+
flask_app = create_app()
|
|
165
|
+
|
|
166
|
+
# AND an invalid request missing required fields
|
|
167
|
+
request_body = {
|
|
168
|
+
"inputs": [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# WHEN we make a request to the async-exec route
|
|
172
|
+
with flask_app.test_client() as test_client:
|
|
173
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
174
|
+
|
|
175
|
+
# THEN we should get a 400 response
|
|
176
|
+
assert response.status_code == 400
|
|
177
|
+
|
|
178
|
+
# AND the response should contain error details
|
|
179
|
+
assert "detail" in response.json
|
|
180
|
+
assert "Invalid context" in response.json["detail"]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_async_exec_route__missing_files():
|
|
184
|
+
"""
|
|
185
|
+
Tests that the async-exec route returns 400 when files are missing.
|
|
186
|
+
"""
|
|
187
|
+
# GIVEN a Flask application
|
|
188
|
+
flask_app = create_app()
|
|
189
|
+
|
|
190
|
+
span_id = uuid4()
|
|
191
|
+
request_body = {
|
|
192
|
+
"execution_id": str(span_id),
|
|
193
|
+
"inputs": [],
|
|
194
|
+
"environment_api_key": "test",
|
|
195
|
+
"module": "workflow",
|
|
196
|
+
"timeout": 360,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# WHEN we make a request to the async-exec route
|
|
200
|
+
with flask_app.test_client() as test_client:
|
|
201
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
202
|
+
|
|
203
|
+
# THEN we should get a 400 response
|
|
204
|
+
assert response.status_code == 400
|
|
205
|
+
|
|
206
|
+
# AND the response should contain error details
|
|
207
|
+
assert "detail" in response.json
|
|
208
|
+
assert "Invalid context" in response.json["detail"]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_async_exec_route__with_syntax_error_in_workflow():
|
|
212
|
+
"""
|
|
213
|
+
Tests that the async-exec route handles workflows with syntax errors gracefully.
|
|
214
|
+
"""
|
|
215
|
+
# GIVEN a Flask application
|
|
216
|
+
flask_app = create_app()
|
|
217
|
+
|
|
218
|
+
span_id = uuid4()
|
|
219
|
+
request_body = {
|
|
220
|
+
"execution_id": str(span_id),
|
|
221
|
+
"inputs": [],
|
|
222
|
+
"environment_api_key": "test",
|
|
223
|
+
"module": "workflow",
|
|
224
|
+
"timeout": 360,
|
|
225
|
+
"files": {
|
|
226
|
+
"__init__.py": "",
|
|
227
|
+
"workflow.py": """\
|
|
228
|
+
from vellum.workflows import BaseWorkflow
|
|
229
|
+
|
|
230
|
+
class Workflow(BaseWorkflow)
|
|
231
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
232
|
+
foo = "hello"
|
|
233
|
+
""",
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# WHEN we make a request to the async-exec route
|
|
238
|
+
with flask_app.test_client() as test_client:
|
|
239
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
240
|
+
|
|
241
|
+
# THEN we should get a 200 response (async execution is accepted)
|
|
242
|
+
assert response.status_code == 200
|
|
243
|
+
|
|
244
|
+
# AND the response should indicate success
|
|
245
|
+
assert response.json == {"success": True}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_async_exec_route__with_invalid_inputs():
|
|
249
|
+
"""
|
|
250
|
+
Tests that the async-exec route handles workflows with invalid inputs gracefully.
|
|
251
|
+
"""
|
|
252
|
+
# GIVEN a Flask application
|
|
253
|
+
flask_app = create_app()
|
|
254
|
+
|
|
255
|
+
span_id = uuid4()
|
|
256
|
+
request_body = {
|
|
257
|
+
"execution_id": str(span_id),
|
|
258
|
+
"inputs": [],
|
|
259
|
+
"environment_api_key": "test",
|
|
260
|
+
"module": "workflow",
|
|
261
|
+
"timeout": 360,
|
|
262
|
+
"files": {
|
|
263
|
+
"__init__.py": "",
|
|
264
|
+
"workflow.py": """\
|
|
265
|
+
from vellum.workflows import BaseWorkflow
|
|
266
|
+
from vellum.workflows.state import BaseState
|
|
267
|
+
from .inputs import Inputs
|
|
268
|
+
|
|
269
|
+
class Workflow(BaseWorkflow[Inputs, BaseState]):
|
|
270
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
271
|
+
foo = "hello"
|
|
272
|
+
""",
|
|
273
|
+
"inputs.py": """\
|
|
274
|
+
from vellum.workflows.inputs import BaseInputs
|
|
275
|
+
|
|
276
|
+
class Inputs(BaseInputs):
|
|
277
|
+
foo: str
|
|
278
|
+
""",
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# WHEN we make a request to the async-exec route
|
|
283
|
+
with flask_app.test_client() as test_client:
|
|
284
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
285
|
+
|
|
286
|
+
# THEN we should get a 200 response (async execution is accepted)
|
|
287
|
+
assert response.status_code == 200
|
|
288
|
+
|
|
289
|
+
# AND the response should indicate success
|
|
290
|
+
assert response.json == {"success": True}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_async_exec_route__background_thread_completes(caplog):
|
|
294
|
+
"""
|
|
295
|
+
Verifies that the async background worker thread runs to completion.
|
|
296
|
+
"""
|
|
297
|
+
# GIVEN a Flask application with log capture enabled
|
|
298
|
+
caplog.set_level(logging.INFO, logger="workflow_server.api.workflow_view")
|
|
299
|
+
flask_app = create_app()
|
|
300
|
+
|
|
301
|
+
baseline = get_active_process_count()
|
|
302
|
+
|
|
303
|
+
# AND a valid workflow request
|
|
304
|
+
span_id = uuid4()
|
|
305
|
+
request_body = {
|
|
306
|
+
"execution_id": str(span_id),
|
|
307
|
+
"inputs": [],
|
|
308
|
+
"environment_api_key": "test",
|
|
309
|
+
"module": "workflow",
|
|
310
|
+
"timeout": 360,
|
|
311
|
+
"files": {
|
|
312
|
+
"__init__.py": "",
|
|
313
|
+
"workflow.py": """\
|
|
314
|
+
from vellum.workflows import BaseWorkflow
|
|
315
|
+
|
|
316
|
+
class Workflow(BaseWorkflow):
|
|
317
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
318
|
+
foo = "hello"
|
|
319
|
+
""",
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# WHEN we call the async-exec route
|
|
324
|
+
with flask_app.test_client() as test_client:
|
|
325
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
326
|
+
|
|
327
|
+
# THEN we get immediate acceptance
|
|
328
|
+
assert response.status_code == 200
|
|
329
|
+
assert response.json == {"success": True}
|
|
330
|
+
|
|
331
|
+
# AND the background thread should complete
|
|
332
|
+
completion_deadline = time.time() + 15
|
|
333
|
+
saw_completion_log = False
|
|
334
|
+
while time.time() < completion_deadline:
|
|
335
|
+
if any("Workflow async exec completed" in rec.message for rec in caplog.records):
|
|
336
|
+
saw_completion_log = True
|
|
337
|
+
break
|
|
338
|
+
time.sleep(0.1)
|
|
339
|
+
|
|
340
|
+
# THEN we should observe the completion log
|
|
341
|
+
assert saw_completion_log, "Did not observe background completion log within 15 seconds"
|
|
342
|
+
|
|
343
|
+
cleanup_deadline = time.time() + 15
|
|
344
|
+
process_count_returned = False
|
|
345
|
+
while time.time() < cleanup_deadline:
|
|
346
|
+
current_count = get_active_process_count()
|
|
347
|
+
if current_count == baseline:
|
|
348
|
+
process_count_returned = True
|
|
349
|
+
break
|
|
350
|
+
time.sleep(0.1)
|
|
351
|
+
|
|
352
|
+
current_count = get_active_process_count()
|
|
353
|
+
assert process_count_returned, (
|
|
354
|
+
f"Process count did not return to baseline within 15 seconds after completion log. "
|
|
355
|
+
f"Expected: {baseline}, Current: {current_count}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_async_exec_route__background_thread_completes_on_error(caplog):
|
|
360
|
+
"""
|
|
361
|
+
Verifies that the background worker completes even when the workflow fails early.
|
|
362
|
+
"""
|
|
363
|
+
# GIVEN a Flask application with log capture enabled
|
|
364
|
+
caplog.set_level(logging.INFO, logger="workflow_server.api.workflow_view")
|
|
365
|
+
flask_app = create_app()
|
|
366
|
+
|
|
367
|
+
baseline = get_active_process_count()
|
|
368
|
+
|
|
369
|
+
span_id = uuid4()
|
|
370
|
+
request_body = {
|
|
371
|
+
"execution_id": str(span_id),
|
|
372
|
+
"inputs": [],
|
|
373
|
+
"environment_api_key": "test",
|
|
374
|
+
"module": "workflow",
|
|
375
|
+
"timeout": 360,
|
|
376
|
+
"files": {
|
|
377
|
+
"__init__.py": "",
|
|
378
|
+
"workflow.py": """\
|
|
379
|
+
from vellum.workflows import BaseWorkflow
|
|
380
|
+
|
|
381
|
+
class Workflow(BaseWorkflow)
|
|
382
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
383
|
+
foo = "hello"
|
|
384
|
+
""",
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# WHEN we call the async-exec route
|
|
389
|
+
with flask_app.test_client() as test_client:
|
|
390
|
+
response = test_client.post("/workflow/async-exec", json=request_body)
|
|
391
|
+
|
|
392
|
+
# THEN we get immediate acceptance
|
|
393
|
+
assert response.status_code == 200
|
|
394
|
+
assert response.json == {"success": True}
|
|
395
|
+
|
|
396
|
+
# AND the background thread should complete and clean up resources
|
|
397
|
+
deadline = time.time() + 15
|
|
398
|
+
process_count_returned = False
|
|
399
|
+
while time.time() < deadline:
|
|
400
|
+
current_count = get_active_process_count()
|
|
401
|
+
if current_count == baseline:
|
|
402
|
+
process_count_returned = True
|
|
403
|
+
break
|
|
404
|
+
time.sleep(0.1)
|
|
405
|
+
|
|
406
|
+
current_count = get_active_process_count()
|
|
407
|
+
assert process_count_returned, (
|
|
408
|
+
f"Process count did not return to baseline on error within 15 seconds. "
|
|
409
|
+
f"Expected: {baseline}, Current: {current_count}"
|
|
410
|
+
)
|
|
@@ -5,6 +5,7 @@ import io
|
|
|
5
5
|
import json
|
|
6
6
|
from queue import Empty
|
|
7
7
|
import re
|
|
8
|
+
import time
|
|
8
9
|
from unittest import mock
|
|
9
10
|
from uuid import uuid4
|
|
10
11
|
|
|
@@ -133,6 +134,8 @@ class Workflow(BaseWorkflow):
|
|
|
133
134
|
|
|
134
135
|
with mock.patch("builtins.open", mock.mock_open(read_data="104857600")):
|
|
135
136
|
# WHEN we call the stream route
|
|
137
|
+
ts_ns = time.time_ns()
|
|
138
|
+
request_body["vembda_service_initiated_timestamp"] = ts_ns
|
|
136
139
|
status_code, events = both_stream_types(request_body)
|
|
137
140
|
|
|
138
141
|
# THEN we get a 200 response
|
|
@@ -174,6 +177,17 @@ class Workflow(BaseWorkflow):
|
|
|
174
177
|
assert "sdk_version" in server_metadata
|
|
175
178
|
assert "memory_usage_mb" in server_metadata
|
|
176
179
|
assert isinstance(server_metadata["memory_usage_mb"], (int, float))
|
|
180
|
+
assert "is_new_server" in server_metadata
|
|
181
|
+
assert server_metadata["is_new_server"] is False
|
|
182
|
+
|
|
183
|
+
# AND the initiated event should have initiated_latency within a reasonable range
|
|
184
|
+
assert "initiated_latency" in server_metadata, "initiated_latency should be present in server_metadata"
|
|
185
|
+
initiated_latency = server_metadata["initiated_latency"]
|
|
186
|
+
assert isinstance(initiated_latency, int), "initiated_latency should be an integer (nanoseconds)"
|
|
187
|
+
# Latency should be positive and less than 60 seconds (60_000_000_000 nanoseconds) for CI
|
|
188
|
+
assert (
|
|
189
|
+
0 < initiated_latency < 60_000_000_000
|
|
190
|
+
), f"initiated_latency should be between 0 and 60 seconds, got {initiated_latency} ns"
|
|
177
191
|
|
|
178
192
|
assert events[2]["name"] == "workflow.execution.fulfilled", events[2]
|
|
179
193
|
assert events[2]["body"]["workflow_definition"]["module"] == ["test", "workflow"]
|
|
@@ -384,9 +398,15 @@ class State(BaseState):
|
|
|
384
398
|
def test_stream_workflow_route__bad_indent_in_inputs_file(both_stream_types):
|
|
385
399
|
# GIVEN a valid request body
|
|
386
400
|
span_id = uuid4()
|
|
401
|
+
trace_id = uuid4()
|
|
402
|
+
parent_span_id = uuid4()
|
|
387
403
|
request_body = {
|
|
388
404
|
"timeout": 360,
|
|
389
405
|
"execution_id": str(span_id),
|
|
406
|
+
"execution_context": {
|
|
407
|
+
"trace_id": str(trace_id),
|
|
408
|
+
"parent_context": {"span_id": str(parent_span_id)},
|
|
409
|
+
},
|
|
390
410
|
"inputs": [
|
|
391
411
|
{"name": "foo", "type": "STRING", "value": "hello"},
|
|
392
412
|
],
|
|
@@ -423,7 +443,7 @@ from vellum.workflows.inputs import BaseInputs
|
|
|
423
443
|
|
|
424
444
|
assert events[0] == {
|
|
425
445
|
"id": mock.ANY,
|
|
426
|
-
"trace_id":
|
|
446
|
+
"trace_id": str(trace_id),
|
|
427
447
|
"span_id": str(span_id),
|
|
428
448
|
"timestamp": mock.ANY,
|
|
429
449
|
"api_version": "2024-10-25",
|
|
@@ -437,9 +457,19 @@ from vellum.workflows.inputs import BaseInputs
|
|
|
437
457
|
}
|
|
438
458
|
|
|
439
459
|
assert events[1]["name"] == "workflow.execution.initiated"
|
|
460
|
+
assert events[1]["trace_id"] == str(trace_id), "workflow initiated event should use request trace_id"
|
|
461
|
+
assert events[1]["parent"] is not None, "workflow initiated event should have parent context"
|
|
462
|
+
assert events[1]["parent"]["span_id"] == str(
|
|
463
|
+
parent_span_id
|
|
464
|
+
), "workflow initiated event parent should match request parent_context"
|
|
440
465
|
|
|
441
466
|
assert events[2]["name"] == "workflow.execution.rejected"
|
|
467
|
+
assert events[2]["trace_id"] == str(trace_id), "workflow rejected event should use request trace_id"
|
|
442
468
|
assert events[2]["span_id"] == events[1]["span_id"]
|
|
469
|
+
assert events[2]["parent"] is not None, "workflow rejected event should have parent context"
|
|
470
|
+
assert events[2]["parent"]["span_id"] == str(
|
|
471
|
+
parent_span_id
|
|
472
|
+
), "workflow rejected event parent should match request parent_context"
|
|
443
473
|
assert (
|
|
444
474
|
"Syntax Error raised while loading Workflow: "
|
|
445
475
|
"unexpected indent (inputs.py, line 3)" in events[2]["body"]["error"]["message"]
|
|
@@ -1211,3 +1241,72 @@ class InvalidWorkflow(BaseWorkflow):
|
|
|
1211
1241
|
assert events[3]["name"] == "vembda.execution.fulfilled"
|
|
1212
1242
|
assert events[3]["span_id"] == str(span_id)
|
|
1213
1243
|
assert events[3]["body"]["exit_code"] == 0
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
@mock.patch("workflow_server.api.workflow_view.get_is_oom_killed")
|
|
1247
|
+
def test_stream_workflow_route__oom_does_not_set_timed_out_flag(mock_get_is_oom_killed):
|
|
1248
|
+
"""
|
|
1249
|
+
Tests that when an OOM error occurs, we don't set the timed_out flag in the vembda fulfilled event.
|
|
1250
|
+
"""
|
|
1251
|
+
# GIVEN a workflow that takes some time to execute
|
|
1252
|
+
span_id = uuid4()
|
|
1253
|
+
request_body = {
|
|
1254
|
+
"timeout": 10,
|
|
1255
|
+
"execution_id": str(span_id),
|
|
1256
|
+
"inputs": [],
|
|
1257
|
+
"environment_api_key": "test",
|
|
1258
|
+
"module": "workflow",
|
|
1259
|
+
"files": {
|
|
1260
|
+
"__init__.py": "",
|
|
1261
|
+
"workflow.py": """\
|
|
1262
|
+
import time
|
|
1263
|
+
|
|
1264
|
+
from vellum.workflows.nodes.bases.base import BaseNode
|
|
1265
|
+
from vellum.workflows.workflows.base import BaseWorkflow
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
class SlowNode(BaseNode):
|
|
1269
|
+
class Outputs(BaseNode.Outputs):
|
|
1270
|
+
value: str
|
|
1271
|
+
|
|
1272
|
+
def run(self) -> Outputs:
|
|
1273
|
+
time.sleep(2)
|
|
1274
|
+
return self.Outputs(value="hello world")
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
class OOMWorkflow(BaseWorkflow):
|
|
1278
|
+
graph = SlowNode
|
|
1279
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
1280
|
+
final_value = SlowNode.Outputs.value
|
|
1281
|
+
|
|
1282
|
+
""",
|
|
1283
|
+
},
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
# WHEN we mock the OOM killer to trigger after a few checks
|
|
1287
|
+
call_count = [0]
|
|
1288
|
+
|
|
1289
|
+
def mock_oom_side_effect():
|
|
1290
|
+
call_count[0] += 1
|
|
1291
|
+
if call_count[0] > 3:
|
|
1292
|
+
return True
|
|
1293
|
+
return False
|
|
1294
|
+
|
|
1295
|
+
mock_get_is_oom_killed.side_effect = mock_oom_side_effect
|
|
1296
|
+
|
|
1297
|
+
# AND we call the stream route
|
|
1298
|
+
status_code, events = flask_stream(request_body)
|
|
1299
|
+
|
|
1300
|
+
# THEN we get a 200 response
|
|
1301
|
+
assert status_code == 200
|
|
1302
|
+
|
|
1303
|
+
# AND we get the expected events
|
|
1304
|
+
event_names = [e["name"] for e in events]
|
|
1305
|
+
|
|
1306
|
+
assert "vembda.execution.initiated" in event_names
|
|
1307
|
+
|
|
1308
|
+
# THEN the key assertion: if there's a vembda.execution.fulfilled event, it should NOT have timed_out=True
|
|
1309
|
+
vembda_fulfilled_event = next(e for e in events if e["name"] == "vembda.execution.fulfilled")
|
|
1310
|
+
assert (
|
|
1311
|
+
vembda_fulfilled_event["body"].get("timed_out") is not True
|
|
1312
|
+
), "timed_out flag should not be set when OOM occurs"
|