Skip to content

Conversation

@GSmithApps
Copy link

@GSmithApps GSmithApps commented Jan 13, 2025

Providing some optional changes while I was taking the course. Cheers!

The bottom half of the diffs are about changing the logic of the Workflow a bit. It probably warrants more discussion if you're interested. For example, I felt confused when I worked through the following code. For example, this internal loop seems to only run once because it has a return statement. And based on the TS example, i was thinking we wanted to be able to send a signal with False that means we couldn't fulfill, and the workflow should stop and not bill the customer, and I don't think this code supports that

    @workflow.run
    async def order_pizza(self, order: PizzaOrder) -> OrderConfirmation:

        # ...

        while True:
            await workflow.wait_condition(
                lambda: not self._pending_confirmation.empty()
            )

            while not self._pending_confirmation.empty():
                bill = Bill(
                    customer_id=order.customer.customer_id,
                    order_number=order.order_number,
                    description="Pizza order",
                    amount=total_price,
                )
                confirmation = await workflow.execute_activity_method(
                    PizzaOrderActivities.send_bill,
                    bill,
                    start_to_close_timeout=timedelta(seconds=5),
                )
                return confirmation

    @workflow.signal
    async def fulfill_order_signal(self, success: bool) -> None:
        if success == True:
            await self._pending_confirmation.put(True)

Comment on lines -19 to +22
1. Edit the `workflow.py` file. The Workflow runs a blocking `while True` loop that will cause it to wait for either the `exit()` Signal or a `submit_greeting()` Signal before doing anything. A variable called `self._current_state` that provides information about the Workflow Execution is also updated in several places. You will add a Query that returns the status of this variable.
1. Edit the `workflow.py` file.

The Workflow runs a blocking `while True` loop that will cause it to wait for either the `exit()` Signal or a `submit_greeting()` Signal before doing anything. A variable called `self._current_state` that provides information about the Workflow Execution is also updated in several places. You will add a Query that returns the status of this variable.

Copy link
Author

Choose a reason for hiding this comment

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

One time Tom suggested to me to try to separate my instructions from the prose/explanation. That's what I was going for here. But totally up to you 👍

Comment on lines +80 to +86
Notice that because `workflow.py` shuts down the Worker when
the Workflow finishes, you cannot Query the Workflow after you
have Signalled it to exit. In contrast, if you ran the Worker
in one file/terminal and the Workflow in another, the Workflow
would shut down and the Worker would keep running, and you could
Query it even after it has exited.

Copy link
Author

Choose a reason for hiding this comment

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

Can include this if we want. Up to you 👍

Comment on lines -36 to -38
@workflow.query
def current_state_query(self) -> str:
return self._current_state
Copy link
Author

Choose a reason for hiding this comment

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

This was in the file, but I don't think it should be. I think putting it here is part of the exercise

Comment on lines -62 to +59
task_queue="queries",
task_queue="queries-queue",
Copy link
Author

Choose a reason for hiding this comment

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

the workflow ID and the task queue were both called queries, so I just changed the queue to queries-queue to disambiguate. Not necessary -- up to you

greetings: List[str] = []
workflow.logger.info("Running workflow.")
print("Running workflow.")

Copy link
Author

Choose a reason for hiding this comment

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

I know it isn't production code, but I figure we don't want print statements in workflow code

Comment on lines -17 to +24
1. This exercise contains one Client that runs two different Workflows
— `PizzaWorkflow.order_pizza` and `FulfillOrderWorkflow.fulfill_order`. Both
Workflows are defined in `workflow.py`. `PizzaWorkflow.order_pizza` is
designed not to complete its final activity — `send_bill` — until it receives
a Signal from `FulfillOrderWorkflow.fulfill_order`. You'll start by defining
that Signal. Edit `workflow.py`. At the bottom of the `PizzaWorkflow`
This exercise contains one Client that runs two different Workflows
— `PizzaWorkflow.order_pizza` and `FulfillOrderWorkflow.fulfill_order`. Both
Workflows are defined in `workflow.py`. `PizzaWorkflow.order_pizza` is
designed not to complete its final activity — `send_bill` — until it receives
a Signal from `FulfillOrderWorkflow.fulfill_order`. You'll start by defining
that Signal.

1. Edit `workflow.py`. At the bottom of the `PizzaWorkflow`
Copy link
Author

Choose a reason for hiding this comment

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

same as above. just separating prose from instruction. totally up to you 👍

class PizzaOrderWorkflow:
def __init__(self) -> None:
self._pending_confirmation: asyncio.Queue[str] = asyncio.Queue()
self._pending_confirmation: asyncio.Queue[bool] = asyncio.Queue()
Copy link
Author

Choose a reason for hiding this comment

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

I think this was a copy-paste thing. The queue is bools

Comment on lines -18 to +19
async def order_pizza(self, order: PizzaOrder) -> OrderConfirmation:
async def order_pizza(self, order: PizzaOrder) -> OrderConfirmation | None:
Copy link
Author

Choose a reason for hiding this comment

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

support if the order was not fulfilled and we don't return a confirmation

from temporalio import workflow
from temporalio.exceptions import ApplicationError

# Import activity, passing it through the sandbox without reloading the module
Copy link
Author

@GSmithApps GSmithApps Jan 13, 2025

Choose a reason for hiding this comment

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

These 👇 are the same changes as above, but in the solution instead of the practice

Comment on lines -40 to +44
while True:
await workflow.wait_condition(
lambda: not self._pending_confirmation.empty()
)
await workflow.wait_condition(
lambda: self._signal_received,
timeout=3
)
Copy link
Author

@GSmithApps GSmithApps Jan 13, 2025

Choose a reason for hiding this comment

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

I have a feeling I'm missing something, but I don't think we need to loop. Like, if we want to just get the signal and if it's yes or no, bill the customer accordingly, then I don't think we need it. But if we want to sort of wait for multiple signals, then maybe we'd need a loop. Curious what you think

Or maybe there's something in the readme or course content that I missed

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this is kind of a weird loop implementation, but based on earlier conversations with the Python SDK team and trying to align with our samples, Python really likes to use loops and queues for Signal detection. I'd like to take everything in this PR except these last two changes -- yes they're a bit weird, but I think they illustrate some expandable patterns.

Copy link

@dandavison dandavison Apr 9, 2025

Choose a reason for hiding this comment

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

Interesting discussion!

Regarding the differences between main and @GSmithApps's PR, there is a flaw in the Python SDK (temporalio/sdk-python#618) which means that the outer while loop is indeed technically necessary. However, I am not seeing a need for the inner while loop: if we enter it then we return or throw and hence never re-enter it.

At a higher level though, my take is that the current code (in main) is handling the signal in a Go-like style: pushing the work into a queue which is processed in the main workflow body. And I would say that that is not idiomatic for the Python SDK (or any SDK other than Go): the purpose of handler functions is that the implementation details of handling can be isolated in the handler function. (There are advantages other than modularity and implementation-hiding, e.g. the handling can more easily be instrumented with observability emissions if it is done in a handler function). Effectively, distinct handler invocations form their own queue.

So, my question is: is there a reason why these materials are encouraging the queue style? If not, then I think we should go with neither the code in main, nor the adjustments made in @GSmithApps's PR, but rather switch to the handler-based implementation.

@drewhoskins-temporal addressed this topic in a recent blog post, as you probably know: https://temporal.io/blog/robust-message-handlers#the-battle-between-handlers-and-queues

Comment on lines -45 to +58
while not self._pending_confirmation.empty():
bill = Bill(
customer_id=order.customer.customer_id,
order_number=order.order_number,
description="Pizza order",
amount=total_price,
)
confirmation = await workflow.execute_activity_method(
PizzaOrderActivities.send_bill,
bill,
start_to_close_timeout=timedelta(seconds=5),
)
return confirmation
if self._pending_confirmation.get_nowait():
bill = Bill(
customer_id=order.customer.customer_id,
order_number=order.order_number,
description="Pizza order",
amount=total_price,
)
confirmation = await workflow.execute_activity_method(
PizzaOrderActivities.send_bill,
bill,
start_to_close_timeout=timedelta(seconds=5),
)
return confirmation
Copy link
Author

Choose a reason for hiding this comment

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

my understanding is that we want to check the signal for true or false, then bill the customer accordingly

Copy link
Contributor

Choose a reason for hiding this comment

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

See above comment. And thanks for this!

@GSmithApps GSmithApps marked this pull request as ready for review March 18, 2025 17:18
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.

4 participants