Skip to content

Commit 24fea4c

Browse files
authored
Expose history size and continue-as-new-suggested (#361)
1 parent 63f63ae commit 24fea4c

File tree

4 files changed

+113
-2
lines changed

4 files changed

+113
-2
lines changed

temporalio/worker/_workflow_instance.py

+10
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ def __init__(self, det: WorkflowInstanceDetails) -> None:
175175
self._time_ns = 0
176176
self._cancel_requested = False
177177
self._current_history_length = 0
178+
self._current_history_size = 0
179+
self._continue_as_new_suggested = False
178180
# Lazily loaded
179181
self._memo: Optional[Mapping[str, Any]] = None
180182
# Handles which are ready to run on the next event loop iteration
@@ -272,6 +274,8 @@ def activate(
272274
self._current_completion.successful.SetInParent()
273275
self._current_activation_error: Optional[Exception] = None
274276
self._current_history_length = act.history_length
277+
self._current_history_size = act.history_size_bytes
278+
self._continue_as_new_suggested = act.continue_as_new_suggested
275279
self._time_ns = act.timestamp.ToNanoseconds()
276280
self._is_replaying = act.is_replaying
277281

@@ -738,6 +742,9 @@ def workflow_extern_functions(self) -> Mapping[str, Callable]:
738742
def workflow_get_current_history_length(self) -> int:
739743
return self._current_history_length
740744

745+
def workflow_get_current_history_size(self) -> int:
746+
return self._current_history_size
747+
741748
def workflow_get_external_workflow_handle(
742749
self, id: str, *, run_id: Optional[str]
743750
) -> temporalio.workflow.ExternalWorkflowHandle[Any]:
@@ -760,6 +767,9 @@ def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable]
760767
def workflow_info(self) -> temporalio.workflow.Info:
761768
return self._outbound.info()
762769

770+
def workflow_is_continue_as_new_suggested(self) -> bool:
771+
return self._continue_as_new_suggested
772+
763773
def workflow_is_replaying(self) -> bool:
764774
return self._is_replaying
765775

temporalio/workflow.py

+31
Original file line numberDiff line numberDiff line change
@@ -336,11 +336,34 @@ def _logger_details(self) -> Mapping[str, Any]:
336336
def get_current_history_length(self) -> int:
337337
"""Get the current number of events in history.
338338
339+
Note, this value may not be up to date if accessed inside a query.
340+
339341
Returns:
340342
Current number of events in history (up until the current task).
341343
"""
342344
return _Runtime.current().workflow_get_current_history_length()
343345

346+
def get_current_history_size(self) -> int:
347+
"""Get the current byte size of history.
348+
349+
Note, this value may not be up to date if accessed inside a query.
350+
351+
Returns:
352+
Current byte-size of history (up until the current task).
353+
"""
354+
return _Runtime.current().workflow_get_current_history_size()
355+
356+
def is_continue_as_new_suggested(self) -> bool:
357+
"""Get whether or not continue as new is suggested.
358+
359+
Note, this value may not be up to date if accessed inside a query.
360+
361+
Returns:
362+
True if the server is configured to suggest continue as new and it
363+
is suggested.
364+
"""
365+
return _Runtime.current().workflow_is_continue_as_new_suggested()
366+
344367

345368
@dataclass(frozen=True)
346369
class ParentInfo:
@@ -405,6 +428,10 @@ def workflow_extern_functions(self) -> Mapping[str, Callable]:
405428
def workflow_get_current_history_length(self) -> int:
406429
...
407430

431+
@abstractmethod
432+
def workflow_get_current_history_size(self) -> int:
433+
...
434+
408435
@abstractmethod
409436
def workflow_get_external_workflow_handle(
410437
self, id: str, *, run_id: Optional[str]
@@ -423,6 +450,10 @@ def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable]
423450
def workflow_info(self) -> Info:
424451
...
425452

453+
@abstractmethod
454+
def workflow_is_continue_as_new_suggested(self) -> bool:
455+
...
456+
426457
@abstractmethod
427458
def workflow_is_replaying(self) -> bool:
428459
...

tests/conftest.py

+10
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ async def env(env_type: str) -> AsyncGenerator[WorkflowEnvironment, None]:
7979
dev_server_extra_args=[
8080
"--dynamic-config-value",
8181
"system.forceSearchAttributesCacheRefreshOnRead=true",
82+
"--dynamic-config-value",
83+
f"limit.historyCount.suggestContinueAsNew={CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT}",
8284
]
8385
)
8486
elif env_type == "time-skipping":
@@ -101,3 +103,11 @@ async def worker(
101103
worker = ExternalPythonWorker(env)
102104
yield worker
103105
await worker.close()
106+
107+
108+
CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT = 50
109+
110+
111+
@pytest.fixture
112+
def continue_as_new_suggest_history_count() -> int:
113+
return CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT

tests/worker/test_workflow.py

+62-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ class InfoWorkflow:
132132
async def run(self) -> Dict:
133133
# Convert to JSON and back so it'll stringify un-JSON-able pieces
134134
ret = dataclasses.asdict(workflow.info())
135-
ret["current_history_length"] = workflow.info().get_current_history_length()
136135
return json.loads(json.dumps(ret, default=str))
137136

138137

@@ -158,7 +157,6 @@ async def test_workflow_info(client: Client, env: WorkflowEnvironment):
158157
)
159158
assert info["attempt"] == 1
160159
assert info["cron_schedule"] is None
161-
assert info["current_history_length"] == 3
162160
assert info["execution_timeout"] is None
163161
assert info["namespace"] == client.namespace
164162
assert info["retry_policy"] == json.loads(
@@ -173,6 +171,68 @@ async def test_workflow_info(client: Client, env: WorkflowEnvironment):
173171
assert info["workflow_type"] == "InfoWorkflow"
174172

175173

174+
@dataclass
175+
class HistoryInfo:
176+
history_length: int
177+
history_size: int
178+
continue_as_new_suggested: bool
179+
180+
181+
@workflow.defn
182+
class HistoryInfoWorkflow:
183+
@workflow.run
184+
async def run(self) -> None:
185+
# Just wait forever
186+
await workflow.wait_condition(lambda: False)
187+
188+
@workflow.signal
189+
async def bunch_of_events(self, count: int) -> None:
190+
# Create a lot of one-day timers
191+
for _ in range(count):
192+
asyncio.create_task(asyncio.sleep(60 * 60 * 24))
193+
194+
@workflow.query
195+
def get_history_info(self) -> HistoryInfo:
196+
return HistoryInfo(
197+
history_length=workflow.info().get_current_history_length(),
198+
history_size=workflow.info().get_current_history_size(),
199+
continue_as_new_suggested=workflow.info().is_continue_as_new_suggested(),
200+
)
201+
202+
203+
async def test_workflow_history_info(
204+
client: Client, env: WorkflowEnvironment, continue_as_new_suggest_history_count: int
205+
):
206+
if env.supports_time_skipping:
207+
pytest.skip("Java test server does not support should continue as new")
208+
async with new_worker(client, HistoryInfoWorkflow) as worker:
209+
handle = await client.start_workflow(
210+
HistoryInfoWorkflow.run,
211+
id=f"workflow-{uuid.uuid4()}",
212+
task_queue=worker.task_queue,
213+
)
214+
# Issue query before anything else, which should mean only a history
215+
# size of 3, at least 100 bytes of history, and no continue as new
216+
# suggestion
217+
orig_info = await handle.query(HistoryInfoWorkflow.get_history_info)
218+
assert orig_info.history_length == 3
219+
assert orig_info.history_size > 100
220+
assert not orig_info.continue_as_new_suggested
221+
222+
# Now send a lot of events
223+
await handle.signal(
224+
HistoryInfoWorkflow.bunch_of_events, continue_as_new_suggest_history_count
225+
)
226+
# Send one more event to trigger the WFT update. We have to do this
227+
# because just a query will have a stale representation of history
228+
# counts, but signal forces a new WFT.
229+
await handle.signal(HistoryInfoWorkflow.bunch_of_events, 1)
230+
new_info = await handle.query(HistoryInfoWorkflow.get_history_info)
231+
assert new_info.history_length > continue_as_new_suggest_history_count
232+
assert new_info.history_size > orig_info.history_size
233+
assert new_info.continue_as_new_suggested
234+
235+
176236
@workflow.defn
177237
class SignalAndQueryWorkflow:
178238
def __init__(self) -> None:

0 commit comments

Comments
 (0)