-
Notifications
You must be signed in to change notification settings - Fork 97
[Bug] SandboxWorkflowRunner doesn't use correct Pydantic field types in some cases #207
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
Comments
Sorry, some additional context I forgot to mention. If you install Pydantic with source only (i.e. without Cython binary), you get an error on startup:
|
I do not see where this is used in the reproduction you provided. I don't see any custom data conversion. Can you update the sample code? I will also test it to see why it's choosing one type and not the other. There is a fix we just made in #202 that may have affected type hints on either side of the sandbox. By default we use Pydantic's There is discussion of this at #143. But basically Pydantic doesn't provide a way to get a JSON-able dict but they've been actively talking about it lately. It provides a way to get JSON string (which we'd have to convert back at a perf loss) and a way to get a dict Pydantic can convert to JSON (but is not cross-language compatible for Temporal benefit). I am planning on writing a sample at temporalio/samples-python#25 that will show how to use full Pydantic JSON conversion.
This is a known issue at https://github.com/temporalio/sdk-python#extending-restricted-classes and something we hope to fix. In the meantime you can mark Also, feel free to join us in |
I have replicated with your code and am looking in detail |
Thanks for the speedy reply! I was in the middle of typing out a response but see you are looking in detail. Thanks very much. For context, I was hesitant to mention I just tried adding Update sample here: import asyncio
import dataclasses
from datetime import datetime
from uuid import uuid4
from pydantic import BaseModel
from temporalio import workflow
from temporalio.client import Client
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxRestrictions, SandboxedWorkflowRunner, SandboxMatcher
class Message(BaseModel):
content: datetime
@workflow.defn
class Workflow:
@workflow.run
async def run(self, context: str) -> None:
message = Message(content=workflow.now())
print(f"Message in workflow with {context}: {message} (`content` type is{type(message.content)})\n")
def default_worker(client: Client, task_queue: str) -> Worker:
return Worker(client, workflows=[Workflow], activities=[], task_queue=task_queue)
def relaxed_worker(client: Client, task_queue: str) -> Worker:
restrictions = dataclasses.replace(
SandboxRestrictions.default,
invalid_module_members=dataclasses.replace(
SandboxRestrictions.invalid_module_members_default,
children={
k: v for k, v in SandboxRestrictions.invalid_module_members_default.children.items() if k != "datetime"
}
)
)
runner = SandboxedWorkflowRunner(restrictions=restrictions)
return Worker(client, workflows=[Workflow], activities=[], task_queue=task_queue, workflow_runner=runner)
def passthrough_worker(client: Client, task_queue: str) -> Worker:
restrictions = dataclasses.replace(
SandboxRestrictions.default,
passthrough_modules=SandboxRestrictions.passthrough_modules_default | SandboxMatcher(access={"pydantic"}),
)
runner = SandboxedWorkflowRunner(restrictions=restrictions)
return Worker(client, workflows=[Workflow], activities=[], task_queue=task_queue, workflow_runner=runner)
async def run_the_bug(client: Client, task_queue: str, context: str) -> None:
message = Message(content=datetime.utcnow())
print(f"Message in main with {context}: {message} (`content` type is{type(message.content)})\n")
handle = await client.start_workflow(Workflow.run, context, id=str(uuid4()), task_queue=task_queue)
await handle.result()
async def main():
task_queue = str(uuid4())
client = await Client.connect("temporal:7233", namespace="default-namespace")
async with default_worker(client, task_queue):
await run_the_bug(client, task_queue, "default sandbox")
async with relaxed_worker(client, task_queue):
await run_the_bug(client, task_queue, "relaxed sandbox")
async with passthrough_worker(client, task_queue):
await run_the_bug(client, task_queue, "passthrough sandbox")
if __name__ == "__main__":
asyncio.run(main()) |
Here's my small reproduction: import asyncio
from datetime import datetime
from uuid import uuid4
from pydantic import BaseModel
from temporalio import workflow
from temporalio.exceptions import ApplicationError
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker
class Message(BaseModel):
content: datetime
@workflow.defn
class Workflow:
@workflow.run
async def run(self) -> None:
message = Message(content=workflow.now())
if not isinstance(message.content, datetime):
raise ApplicationError(f"was type {type(message.content)}")
async def main():
async with await WorkflowEnvironment.start_local() as env:
task_queue = str(uuid4())
async with Worker(env.client, workflows=[Workflow], task_queue=task_queue):
await env.client.execute_workflow(
Workflow.run, id=str(uuid4()), task_queue=task_queue
)
if __name__ == "__main__":
asyncio.run(main()) Debugging now... |
I have hunted this issue down. This is caused by https://github.com/temporalio/sdk-python#is_subclass-of-abc-based-restricted-classes:
Also see python/cpython#89010 Basically at https://github.com/pydantic/pydantic/blob/bc74342b39e2b1d0115364cf2aa90d8f874cb9b5/pydantic/validators.py#L751, It might look something like (untested): my_restrictions = dataclasses.replace(
SandboxRestrictions.default,
invalid_module_members=SandboxMatcher(
children={k: v for SandboxRestrictions.invalid_module_members_default.children.items() if k != "datetime"}
),
)
my_worker = Worker(..., workflow_runner=SandboxedWorkflowRunner(restrictions=my_restrictions)) But this removes our protections against calling something like |
Thanks, @cretz, for chasing this down. |
While a fix such as patching I still can't figure out how best to solve this. Researching... |
For now, I have just marked this a known issue in the README in #219. If this becomes enough of an issue, we'll have to consider patching |
Copilot brought me here... My error looks slightly different: ... and then discovered that python-samples contains data converter for Pydantic - that solved the issue for me. Perhaps this issue could be closed?.. |
π Will close. For others arriving here, this is documented in the README and the supported way to use Pydantic is via a custom payload converter as shown in samples. |
For future people investigating problems related to this: We ran into an error related to this and disabling sandboxing on the worker or passing through the datetime and pydantic (v1) imports here ultimately fixed it. Disabling sandboxing on one workflow via the Either way, the guidance on the Readme added as a result of this issue is accurate. Below is the error we got on importing a BaseModel in an activity. The error was specifically around a pydantic
|
Could this be fixed by moving the issubclass for datetime check before the issubclass date check? What is the actual offending line that is causing this to happen? |
What are you really trying to do?
I want to be able to pass in Pydantic models with
datetime
fields.To encode
datetime
fields, I am using thepydantic.json.pydantic_encoder
but can hit this behaviour without any data conversion occurring.Describe the bug
The
SandboxWorkflowRunner
restrictions are resulting in Pydantic models being created with incorrect field types.Specifically, I am seeing models derived from Pydantic's
BaseModel
class (let's sayMyDatetimeWrapper
) that contain adatetime
field being instantiated with adate
object instead when aMyDatetimeWrapper
object is created within a workflow.Minimal Reproduction
I've created a code sample to illustrate the issue:
The output from this snippet it:
As you can see, the default sandbox runner is resulting in
content
being converted to just adate
despite being instantiated with adatetime
.Environment/Versions
The text was updated successfully, but these errors were encountered: