Skip to content

Commit 99e36cb

Browse files
authored
Sandbox and type fixes (#202)
Fixes #200 Fixes #201 Fixes #199 Fixes #198
1 parent 7dedda3 commit 99e36cb

File tree

5 files changed

+69
-20
lines changed

5 files changed

+69
-20
lines changed

temporalio/bridge/runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,5 @@ class TelemetryConfig:
147147
logging: Optional[LoggingConfig] = LoggingConfig.default
148148
"""Logging configuration."""
149149

150-
metrics: Optional[PrometheusConfig] = None
150+
metrics: Optional[MetricsConfig] = None
151151
"""Metrics configuration."""

temporalio/contrib/opentelemetry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
import temporalio.worker
3636
import temporalio.workflow
3737

38+
# OpenTelemetry dynamically, lazily chooses its context implementation at
39+
# runtime. When first accessed, they use pkg_resources.iter_entry_points + load.
40+
# The load uses built-in open() which we don't allow in sandbox mode at runtime,
41+
# only import time. Therefore if the first use of a OTel context is inside the
42+
# sandbox, which it may be for a workflow worker, this will fail. So instead we
43+
# eagerly reference it here to force loading at import time instead of lazily.
44+
opentelemetry.context.get_current()
45+
3846
default_text_map_propagator = opentelemetry.propagators.composite.CompositePropagator(
3947
[
4048
opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator(),

temporalio/worker/workflow_sandbox/_in_sandbox.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import temporalio.bridge.proto.workflow_activation
1212
import temporalio.bridge.proto.workflow_completion
1313
import temporalio.worker._workflow_instance
14+
import temporalio.workflow
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -34,22 +35,10 @@ def __init__(
3435
) -> None:
3536
"""Create in-sandbox instance."""
3637
_trace("Initializing workflow %s in sandbox", workflow_class)
37-
# We have to replace the given instance instance details with new one
38-
# replacing references to the workflow class
39-
old_defn = instance_details.defn
40-
new_defn = dataclasses.replace(
41-
old_defn,
42-
cls=workflow_class,
43-
run_fn=getattr(workflow_class, old_defn.run_fn.__name__),
44-
signals={
45-
k: dataclasses.replace(v, fn=getattr(workflow_class, v.fn.__name__))
46-
for k, v in old_defn.signals.items()
47-
},
48-
queries={
49-
k: dataclasses.replace(v, fn=getattr(workflow_class, v.fn.__name__))
50-
for k, v in old_defn.queries.items()
51-
},
52-
)
38+
# We expect to be able to get the workflow definition back off the
39+
# class. We can't use the definition that was given to us because it has
40+
# type hints and references to outside-of-sandbox types.
41+
new_defn = temporalio.workflow._Definition.must_from_class(workflow_class)
5342
new_instance_details = dataclasses.replace(instance_details, defn=new_defn)
5443

5544
# Instantiate the runner and the instance

temporalio/worker/workflow_sandbox/_restrictions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,9 @@ def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]:
437437
# TODO(cretz): Fix issues with class extensions on restricted proxy
438438
# "argparse": SandboxMatcher.all_uses_runtime,
439439
"bz2": SandboxMatcher(use={"open"}),
440-
"concurrent": SandboxMatcher.all_uses_runtime,
440+
"concurrent": SandboxMatcher(
441+
children={"futures": SandboxMatcher.all_uses_runtime}
442+
),
441443
# Python's own re lib registers itself. This is mostly ok to not
442444
# restrict since it's just global picklers that people may want to
443445
# register globally.

tests/worker/workflow_sandbox/test_runner.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
import time
88
import uuid
99
from dataclasses import dataclass
10-
from datetime import date
10+
from datetime import date, timedelta
11+
from enum import IntEnum
1112
from typing import Callable, Dict, List, Optional, Sequence, Type
1213

1314
import pytest
1415

1516
import temporalio.worker.workflow_sandbox._restrictions
16-
from temporalio import workflow
17+
from temporalio import activity, workflow
1718
from temporalio.client import Client, WorkflowFailureError, WorkflowHandle
1819
from temporalio.exceptions import ApplicationError
1920
from temporalio.worker import Worker
@@ -205,6 +206,8 @@ async def test_workflow_sandbox_restrictions(client: Client):
205206
# General library allowed calls
206207
"import datetime\ndatetime.date(2001, 1, 1)",
207208
"import uuid\nuuid.uuid5(uuid.NAMESPACE_DNS, 'example.com')",
209+
# Other imports we want to allow
210+
"from concurrent.futures import ThreadPoolExecutor",
208211
]
209212
for code in valid_code_to_check:
210213
await client.execute_workflow(
@@ -310,6 +313,53 @@ async def test_workflow_sandbox_access_stack(client: Client):
310313
)
311314

312315

316+
class InstanceCheckEnum(IntEnum):
317+
FOO = 1
318+
BAR = 2
319+
320+
321+
@dataclass
322+
class InstanceCheckData:
323+
some_enum: InstanceCheckEnum
324+
325+
326+
@activity.defn
327+
async def instance_check_activity(param: InstanceCheckData) -> InstanceCheckData:
328+
assert isinstance(param, InstanceCheckData)
329+
assert param.some_enum is InstanceCheckEnum.BAR
330+
return param
331+
332+
333+
@workflow.defn
334+
class InstanceCheckWorkflow:
335+
@workflow.run
336+
async def run(self, param: InstanceCheckData) -> InstanceCheckData:
337+
assert isinstance(param, InstanceCheckData)
338+
assert param.some_enum is InstanceCheckEnum.BAR
339+
# Exec child if not a child, otherwise exec activity
340+
if workflow.info().parent is None:
341+
return await workflow.execute_child_workflow(
342+
InstanceCheckWorkflow.run, param
343+
)
344+
return await workflow.execute_activity(
345+
instance_check_activity,
346+
param,
347+
schedule_to_close_timeout=timedelta(minutes=1),
348+
)
349+
350+
351+
async def test_workflow_sandbox_instance_check(client: Client):
352+
async with new_worker(
353+
client, InstanceCheckWorkflow, activities=[instance_check_activity]
354+
) as worker:
355+
await client.execute_workflow(
356+
InstanceCheckWorkflow.run,
357+
InstanceCheckData(some_enum=InstanceCheckEnum.BAR),
358+
id=f"workflow-{uuid.uuid4()}",
359+
task_queue=worker.task_queue,
360+
)
361+
362+
313363
def new_worker(
314364
client: Client,
315365
*workflows: Type,

0 commit comments

Comments
 (0)