-
Notifications
You must be signed in to change notification settings - Fork 172
fix(amber): tolerate in-flight RPC acks when EndWorker terminates a worker #6892
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,12 @@ | |
| from loguru import logger | ||
|
|
||
| from core.architecture.handlers.control.control_handler_base import ControlHandler | ||
| from core.models.internal_queue import InternalQueue | ||
| from core.models.internal_queue import DCMElement, InternalQueue | ||
| from core.util.proto import get_one_of | ||
| from proto.org.apache.texera.amber.engine.architecture.rpc import ( | ||
| EmptyReturn, | ||
| EmptyRequest, | ||
| ReturnInvocation, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -37,19 +39,45 @@ async def end_worker(self, req: EmptyRequest) -> EmptyReturn: | |
| has finished not only the data processing logic, but also the processing | ||
| of all the control messages. | ||
| """ | ||
| # Ensure this is really the last message. Read the queued count once (InternalQueue | ||
| # exposes size(); the base IQueue interface does not) and branch on it. | ||
| # Ensure this is really the last message that carries work. Read the | ||
| # queued count once (InternalQueue exposes size(); the base IQueue | ||
| # interface does not) and branch on it. | ||
| input_queue: InternalQueue = self.context.input_queue | ||
| queued_count = input_queue.size() | ||
| if queued_count > 0: | ||
| if input_queue.size() == 0: | ||
| # Now we can safely acknowledge that this worker can be terminated. | ||
| return EmptyReturn() | ||
|
|
||
| # RPC acks (ReturnInvocations for this worker's own fire-and-forget | ||
| # calls, e.g. worker_execution_completed) race with EndWorker by | ||
| # design: the coordinator decides to end the worker while its acks are | ||
| # still in flight. The worker never awaits those acks, so an ack-only | ||
| # backlog is safe to drop at termination. Anything else fails loudly | ||
| # AND is put back on the queue so the coordinator's retried EndWorker | ||
| # finds it processed (mirrors the Scala EndHandler). | ||
| pending = [] | ||
| while not input_queue.is_empty(): | ||
| pending.append(input_queue.get()) | ||
|
|
||
| def is_ack(element) -> bool: | ||
| return isinstance(element, DCMElement) and isinstance( | ||
| get_one_of(element.payload, sealed=False), ReturnInvocation | ||
| ) | ||
|
|
||
| if all(is_ack(element) for element in pending): | ||
| logger.warning( | ||
| f"Received EndWorker before all {queued_count} queued " | ||
| f"message(s) were processed; failing the RPC so a later " | ||
| f"coordinator retry succeeds once the queue has drained." | ||
| f"Received EndWorker with only RPC acks left in the queue; " | ||
| f"proceeding with termination. Pending acks: {pending}" | ||
| ) | ||
| # Fail this RPC (the counterpart of the Scala EndHandler's | ||
| # Future.exception) so a later coordinator retry succeeds once | ||
| # the queue has drained, instead of dropping the pending message. | ||
| raise RuntimeError("worker still has unprocessed messages") | ||
| # Now we can safely acknowledge that this worker can be terminated. | ||
| return EmptyReturn() | ||
| return EmptyReturn() | ||
|
|
||
| for element in pending: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would change the thread to be a producer (by putting elements into the queue). can we avoid it?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for actor model, we don't allow peek into the next message. |
||
| input_queue.put(element) | ||
| logger.warning( | ||
| f"Received EndWorker before all {len(pending)} queued " | ||
| f"message(s) were processed; failing the RPC so a later " | ||
| f"coordinator retry succeeds once the queue has drained." | ||
| ) | ||
| # Fail this RPC (the counterpart of the Scala EndHandler's | ||
| # Future.exception) so a later coordinator retry succeeds once | ||
| # the queue has drained, instead of dropping the pending message. | ||
| raise RuntimeError("worker still has unprocessed messages") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as discussed offline,
Received EndWorker before all {queued_count} queued message(s) were processedis a hard requirement for EndWorker. if there are flaky scenarios causing this failure, then that means there are some uncleared message do not meet this requirement.I think we should fix those scenarios, instead of relaxing this requirement.