Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Interrupt heartbeating activity on pause #854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

THardy98
Copy link
Contributor

@THardy98 THardy98 commented May 1, 2025

What was changed

Adds support to interrupt heartbeating activities from pause requests.

Why?

Functionally support users pausing their activities via the CLI

  1. Closes Heartbeating activities should be interrupted when the activities are paused. #812

  2. How was this tested:
    Simple integration test

  3. Any docs updates needed?
    Unsure

@THardy98 THardy98 requested a review from a team as a code owner May 1, 2025 07:38
@@ -6250,6 +6258,9 @@ async def heartbeat_async_activity(
)
if resp_by_id.cancel_requested:
raise AsyncActivityCancelledError()
if resp_by_id.activity_paused:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm, looks like we never added "raises" docs to AsyncActivityHandle.heartbeat, wonder if we should now

@THardy98 THardy98 force-pushed the support_activity_pause branch from d5107b0 to 9763505 Compare May 3, 2025 06:35
@THardy98 THardy98 requested a review from cretz May 3, 2025 06:39
@THardy98 THardy98 force-pushed the support_activity_pause branch 2 times, most recently from 5c8299a to 4451e77 Compare May 3, 2025 18:07
@@ -135,6 +138,29 @@ def _logger_details(self) -> Mapping[str, Any]:
_current_context: contextvars.ContextVar[_Context] = contextvars.ContextVar("activity")


@dataclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be frozen

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mutate the fields in this object to reflect changes across running activity & _context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to frozen class + holder

@@ -436,6 +438,7 @@ async def _run_activity(
runtime_metric_meter=None
if sync_non_threaded
else self._metric_meter,
cancellation_details=running_activity.cancellation_details,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context says it is optional, but this is set with a never-None value. I think you may need some kind of "holder" wrapper.

Copy link
Contributor Author

@THardy98 THardy98 May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be preferred over a non-optional field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to frozen class + holder

Comment on lines 99 to 100
if cancellation_details:
self.cancellation_details.set_details(cancellation_details)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to always be set, even if not provided. May want to change param to be non-option w/ a default of temporalio.activity.ActivityCancellationDetails(). Usually using a default param of a full expression is bad in Python because it isn't called for each call, but since the details are immutable, sharing the default for everyone is safe.

Copy link
Contributor Author

@THardy98 THardy98 May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to always be set, even if not provided.

Why?

May want to change param to be non-option w/ a default of temporalio.activity.ActivityCancellationDetails()

Sort of defeats the purpose of being able to return None without checking all fields are False, in which case, maybe the holder indirection isn't so useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic needs to be "you can guarantee activity.cancellation_details() is not None when the activity is being canceled", but this breaks that guarantee.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolving, I think this is still an issue. We should always set an instance of this on cancel in test environment. So the parameter should default to an empty instance with no optional accepted.

Comment on lines 7438 to 7647
except (CancelledError, asyncio.CancelledError):
return activity.cancellation_details()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need some tests to confirm what happens when this is not caught

Copy link
Contributor Author

@THardy98 THardy98 May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test for pausing/unpausing activities, we don't catch cancel errors.

Added the unpause activity method to the bridge client - it was missing.

I made it such that a cancelled error on activity pause returns an activity task failure w/ application failure:

  • When going through the default failure converter, a canceled error populates cancel failure info, but core expects application failure info because we are populating the failed field of the completion instead of cancel.
  • A bit of a weird situation, because we can't populate cancel for the reason you mentioned above (we tell server to cancel and server complains that a cancel hasn't been requested yet - like you mention above)

I can revert this change if you prefer, I don't think it causes any notable server effect anyway (state of paused activities are suspended server-side, iiuc), but probably a good thing to have failures sent properly just as a general rule

@THardy98 THardy98 requested a review from cretz May 6, 2025 18:00
Comment on lines 83 to 85
cancellation_details: Optional[
temporalio.activity.ActivityCancellationDetails
] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated #854 (comment) thread, I think this parameter should not be Optional

Copy link
Contributor Author

@THardy98 THardy98 May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops didn't mean to resolve - param is non-optional. Updated callers.

@THardy98 THardy98 force-pushed the support_activity_pause branch 2 times, most recently from c5ca36e to c3634fe Compare May 6, 2025 20:58
def cancel(self) -> None:
def cancel(
self,
cancellation_details: temporalio.activity.ActivityCancellationDetails,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, providing the parameter still needs to be optional (this is a backwards incompatible change otherwise), it just needs to not be Optional. So:

Suggested change
cancellation_details: temporalio.activity.ActivityCancellationDetails,
cancellation_details: temporalio.activity.ActivityCancellationDetails = temporalio.activity.ActivityCancellationDetails(cancel_requested=True),

I just picked cancel_requested as the default (might as well set one to True by default). Usually having an expression as a parameter default like this in Python is bad because that same instance is reused for every default so mutations are shared, but this object is immutable so it's safe.

Sorry for the confusion

@THardy98 THardy98 force-pushed the support_activity_pause branch from c3634fe to 58bd687 Compare May 16, 2025 15:31
@THardy98 THardy98 force-pushed the support_activity_pause branch from 4977651 to e055766 Compare May 16, 2025 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Heartbeating activities should be interrupted when the activities are paused.
3 participants