diff --git a/docs/develop/plugins-guide.mdx b/docs/develop/plugins-guide.mdx index 0f51224a81..2980fef313 100644 --- a/docs/develop/plugins-guide.mdx +++ b/docs/develop/plugins-guide.mdx @@ -62,17 +62,113 @@ You should refer to the [best practices for creating Activities](/activity-defin Temporal's Activity retry mechanism gives applications the benefits of durable execution. See the [Activity retry policy explanation](/activity-definition#activity-retry-policy) for more details. -Here is an example with Python: - -```python + + + {/* SNIPSTART python-plugin-activity */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py @activity.defn async def some_activity() -> None: - return None + return None -plugin = SimplePlugin( - activities = [some_activity] + +plugin = SimplePlugin("PluginName", activities=[some_activity]) +``` + {/* SNIPEND */} + + + {/* SNIPSTART go-plugin-activity */} +[features/snippets/plugins/plugins.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.go) +```go +func SomeActivity(ctx context.Context) error { + // Activity implementation + return nil +} + +func createActivityPlugin() (*temporal.SimplePlugin, error) { + return temporal.NewSimplePlugin(temporal.SimplePluginOptions{ + Name: "PluginName", + RunContextBefore: func(ctx context.Context, options temporal.SimplePluginRunContextBeforeOptions) error { + options.Registry.RegisterActivityWithOptions( + SomeActivity, + activity.RegisterOptions{Name: "SomeActivity"}, + ) + return nil + }, + }) +} + +``` + {/* SNIPEND */} + + + {/* SNIPSTART java-plugin-activity */} +[features/snippets/plugins/plugins.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.java) +```java +@ActivityInterface +public interface SomeActivity { + @ActivityMethod + void someActivity(); +} + +public class SomeActivityImpl implements SomeActivity { + @Override + public void someActivity() { + // Activity implementation + } +} + +SimplePlugin activityPlugin = + SimplePlugin.newBuilder("PluginName") + .registerActivitiesImplementations(new SomeActivityImpl()) + .build(); +``` + {/* SNIPEND */} + + + {/* SNIPSTART typescript-plugins-activity */} +[features/snippets/plugins/plugins.ts](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.ts) +```ts +const activity = async () => 'activity'; +const plugin = new SimplePlugin({ + name: 'plugin-name', + activities: { + pluginActivity: activity, + }, +}); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-plugins-activity */} +[features/snippets/plugins/plugins.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.cs) +```cs +[Activity] +static void SomeActivity() => throw new NotImplementedException(); + +SimplePlugin activityPlugin = new SimplePlugin( + "PluginName", + new SimplePluginOptions() { }.AddActivity(SomeActivity)); +``` + {/* SNIPEND */} + + + {/* SNIPSTART ruby-plugin-activity */} +[features/snippets/plugins/plugins.rb](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.rb) +```rb +def some_activity + # Activity implementation +end + +plugin = Temporalio::SimplePlugin.new( + name: 'PluginName', + activities: [method(:some_activity)] ) ``` + {/* SNIPEND */} + + + ### Workflow-friendly libraries @@ -118,63 +214,274 @@ Consider adding a Child Workflow when one or more of these conditions applies: Any Workflow can be run as a standalone Workflow or as a Child Workflow, so registering a Child Workflow in a `SimplePlugin` is the same as registering any Workflow. -Here is an example with Python: - -```python + + + {/* SNIPSTART python-plugin-workflow */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py @workflow.defn class HelloWorkflow: - @workflow.run - async def run(self, name: str) -> str: - return f"Hello, {name}!" + @workflow.run + async def run(self, name: str) -> str: + return f"Hello, {name}!" -plugin = SimplePlugin( - workflows = [HelloWorkflow] -) - -... - -client = await Client.connect( - "localhost:7233", - plugins=[ - plugin, - ], + +plugin = SimplePlugin("PluginName", workflows=[HelloWorkflow]) +``` + {/* SNIPEND */} + + + {/* SNIPSTART go-plugin-workflow */} +[features/snippets/plugins/plugins.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.go) +```go +func HelloWorkflow(ctx workflow.Context, name string) (string, error) { + return "Hello, " + name + "!", nil +} + +func createWorkflowPlugin() (*temporal.SimplePlugin, error) { + return temporal.NewSimplePlugin(temporal.SimplePluginOptions{ + Name: "PluginName", + RunContextBefore: func(ctx context.Context, options temporal.SimplePluginRunContextBeforeOptions) error { + options.Registry.RegisterWorkflowWithOptions( + HelloWorkflow, + workflow.RegisterOptions{Name: "HelloWorkflow"}, + ) + return nil + }, + }) +} + +``` + {/* SNIPEND */} + + + {/* SNIPSTART java-plugin-workflow */} +[features/snippets/plugins/plugins.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.java) +```java +@WorkflowInterface +public interface HelloWorkflow { + @WorkflowMethod + String run(String name); +} + +public static class HelloWorkflowImpl implements HelloWorkflow { + @Override + public String run(String name) { + return "Hello, " + name + "!"; + } +} + +SimplePlugin workflowPlugin = + SimplePlugin.newBuilder("PluginName") + .registerWorkflowImplementationTypes(HelloWorkflowImpl.class) + .build(); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-plugins-workflow */} +[features/snippets/plugins/plugins.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.cs) +```cs +[Workflow] +class SimpleWorkflow +{ + [WorkflowRun] + public Task RunAsync(string name) => Task.FromResult($"Hello, {name}!"); +} + +SimplePlugin workflowPlugin = new SimplePlugin( + "PluginName", + new SimplePluginOptions() { }.AddWorkflow()); +``` + {/* SNIPEND */} + + + {/* SNIPSTART ruby-plugin-workflow */} +[features/snippets/plugins/plugins.rb](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.rb) +```rb +class HelloWorkflow < Temporalio::Workflow::Definition + def execute(name) + "Hello, #{name}!" + end +end + +plugin = Temporalio::SimplePlugin.new( + name: 'PluginName', + workflows: [HelloWorkflow] ) -async with Worker( - client, - task_queue="task-queue", -): - client.execute_workflow( - HelloWorkflow.run, - "Tim", - task_queue=worker.task_queue, - ) ``` + {/* SNIPEND */} + + ### Built-in Nexus Operations Nexus calls are used from Workflows similar to Activities and you can check out some common [Nexus Use Cases](/nexus/use-cases). Like Activities, Nexus Call arguments and return values must be serializable. -Here's an example of how to register Nexus handlers in Workflows with Python: - -```python + + + {/* SNIPSTART python-plugin-nexus */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py @nexusrpc.service class WeatherService: - get_weather_nexus_operation: nexusrpc.Operation[WeatherInput, Weather] + get_weather_nexus_operation: nexusrpc.Operation[WeatherInput, Weather] + @nexusrpc.handler.service_handler(service=WeatherService) class WeatherServiceHandler: - @nexusrpc.handler.sync_operation - async def get_weather_nexus_operation( - self, ctx: nexusrpc.handler.StartOperationContext, input: WeatherInput - ) -> Weather: - return Weather( - city=input.city, temperature_range="14-20C", conditions="Sunny with wind." - ) - -plugin = SimplePlugin( - nexus_service_handlers = [WeatherServiceHandler()] + @nexusrpc.handler.sync_operation + async def get_weather_nexus_operation( + self, ctx: nexusrpc.handler.StartOperationContext, input: WeatherInput + ) -> Weather: + return Weather( + city=input.city, + temperature_range="14-20C", + conditions="Sunny with wind.", + ) + + +plugin = SimplePlugin("PluginName", nexus_service_handlers=[WeatherServiceHandler()]) +``` + {/* SNIPEND */} + + + {/* SNIPSTART go-plugin-nexus */} +[features/snippets/plugins/plugins.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.go) +```go +type WeatherInput struct { + City string `json:"city"` +} + +type Weather struct { + City string `json:"city"` + TemperatureRange string `json:"temperatureRange"` + Conditions string `json:"conditions"` +} + +var WeatherService = nexus.NewService("weather-service") + +var GetWeatherOperation = nexus.NewSyncOperation( + "get-weather", + func(ctx context.Context, input WeatherInput, options nexus.StartOperationOptions) (Weather, error) { + return Weather{ + City: input.City, + TemperatureRange: "14-20C", + Conditions: "Sunny with wind.", + }, nil + }, ) + +func createNexusPlugin() (*temporal.SimplePlugin, error) { + return temporal.NewSimplePlugin(temporal.SimplePluginOptions{ + Name: "PluginName", + RunContextBefore: func(ctx context.Context, options temporal.SimplePluginRunContextBeforeOptions) error { + options.Registry.RegisterNexusService(WeatherService) + return nil + }, + }) +} + ``` + {/* SNIPEND */} + + + {/* SNIPSTART java-plugin-nexus */} +[features/snippets/plugins/plugins.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.java) +```java +// Example Nexus service implementation +public class WeatherService { + public Weather getWeather(WeatherInput input) { + return new Weather(input.getCity(), "14-20C", "Sunny with wind."); + } +} + +public static class Weather { + private final String city; + private final String temperatureRange; + private final String conditions; + + public Weather(String city, String temperatureRange, String conditions) { + this.city = city; + this.temperatureRange = temperatureRange; + this.conditions = conditions; + } + + // Getters... +} + +public static class WeatherInput { + private final String city; + + public WeatherInput(String city) { + this.city = city; + } + + public String getCity() { + return city; + } +} + +SimplePlugin nexusPlugin = + SimplePlugin.newBuilder("PluginName") + .registerNexusServiceImplementation(new WeatherService()) + .build(); +``` + {/* SNIPEND */} + + + {/* SNIPSTART typescript-plugins-nexus */} +[features/snippets/plugins/plugins.ts](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.ts) +```ts +const testServiceHandler = nexus.serviceHandler( + nexus.service('testService', { + testSyncOp: nexus.operation(), + }), + { + async testSyncOp(_, input) { + return input; + }, + }, +); +const plugin = new SimplePlugin({ + name: 'plugin-name', + nexusServices: [testServiceHandler], +}); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-plugins-nexus */} +[features/snippets/plugins/plugins.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.cs) +```cs +[NexusService] +public interface IStringService +{ + [NexusOperation] + string DoSomething(string name); +} + +[NexusServiceHandler(typeof(IStringService))] +public class HandlerFactoryStringService +{ + private readonly Func> handlerFactory; + + public HandlerFactoryStringService(Func> handlerFactory) => + this.handlerFactory = handlerFactory; + + [NexusOperationHandler] + public IOperationHandler DoSomething() => handlerFactory(); +} + +SimplePlugin nexusPlugin = new SimplePlugin( + "PluginName", + new SimplePluginOptions() { }.AddNexusService(new HandlerFactoryStringService(() => + OperationHandler.Sync((ctx, name) => $"Hello, {name}"))) +); +``` + {/* SNIPEND */} + + ### Custom Data Converters @@ -182,21 +489,116 @@ A [custom Data Converter](/default-custom-data-converters#custom-data-converter) Note that you can use an existing Data Converter such as, in Python, `PydanticPayloadConverter` in your Plugin. -Here's an example of how to add a Custom Data Converter to a Plugin with Python: + + + {/* SNIPSTART python-plugin-converter */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py +def set_converter(converter: DataConverter | None) -> DataConverter: + if converter is None or converter == DataConverter.default: + return pydantic_data_converter + # Should consider interactions with other plugins, + # as this will override the data converter. + # This may mean failing, warning, or something else + return converter + + +plugin = SimplePlugin("PluginName", data_converter=set_converter) +``` + {/* SNIPEND */} + + + {/* SNIPSTART go-plugin-converter */} +[features/snippets/plugins/plugins.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.go) +```go +func createConverterPlugin() (*temporal.SimplePlugin, error) { + customConverter := converter.GetDefaultDataConverter() // Or your custom converter + + return temporal.NewSimplePlugin(temporal.SimplePluginOptions{ + Name: "PluginName", + DataConverter: customConverter, + }) +} -```python -def add_converter(converter: Optional[DataConverter]) -> DataConverter - if converter is None or converter == temporalio.converter.DataConverter.default - return pydantic_data_converter - # Should consider interactions with other plugins, - # as this will override the data converter. - # This may mean failing, warning, or something else - return converter +``` + {/* SNIPEND */} + + + {/* SNIPSTART java-plugin-converter */} +[features/snippets/plugins/plugins.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.java) +```java +SimplePlugin converterPlugin = + SimplePlugin.newBuilder("PluginName") + .customizeDataConverter( + existingConverter -> { + // Customize the data converter + // This example keeps the existing converter unchanged + // In practice, you might wrap it with additional functionality + return existingConverter; + }) + .build(); +``` + {/* SNIPEND */} + + + {/* SNIPSTART typescript-plugins-converter */} +[features/snippets/plugins/plugins.ts](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.ts) +```ts +const codec: PayloadCodec = { + encode(_payloads: Payload[]): Promise { + throw new Error(); + }, + decode(_payloads: Payload[]): Promise { + throw new Error(); + }, +}; +const plugin = new SimplePlugin({ + name: 'plugin-name', + dataConverter: (converter: DataConverter | undefined) => ({ + ...converter, + payloadCodecs: [...(converter?.payloadCodecs ?? []), codec], + }), +}); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-plugins-converter */} +[features/snippets/plugins/plugins.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.cs) +```cs +private class Codec : IPayloadCodec +{ + public Task> EncodeAsync(IReadOnlyCollection payloads) => throw new NotImplementedException(); + public Task> DecodeAsync(IReadOnlyCollection payloads) => throw new NotImplementedException(); +} + +SimplePlugin converterPlugin = new SimplePlugin( + "PluginName", + new SimplePluginOptions() + { + DataConverterOption = new SimplePluginOptions.SimplePluginOption( + (converter) => converter with { PayloadCodec = new Codec() } + ), + }); +``` + {/* SNIPEND */} + + + {/* SNIPSTART ruby-plugin-converter */} +[features/snippets/plugins/plugins.rb](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.rb) +```rb +custom_converter = Temporalio::Converters::DataConverter.new( + payload_converter: Temporalio::Converters::PayloadConverter.default +) -plugin = SimplePlugin( - data_converter = add_converter +plugin = Temporalio::SimplePlugin.new( + name: 'PluginName', + data_converter: custom_converter ) ``` + {/* SNIPEND */} + + ### Interceptors @@ -205,42 +607,191 @@ Interceptors are middleware that can run before and after various calls such as - Create side effects such as logging and tracing. - Modify arguments, such as adding headers for authorization or tracing propagation. -Here's an example of how to add one to a Plugin with Python: + + + {/* SNIPSTART python-plugin-interceptors */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py +class SomeWorkerInterceptor(temporalio.worker.Interceptor): + pass # Your implementation + + +class SomeClientInterceptor(temporalio.client.Interceptor): + pass # Your implementation -```python -class SomeWorkerInterceptor( - temporalio.worker.Interceptor -): - pass # Your implementation plugin = SimplePlugin( - worker_interceptors = [SomeWorkerInterceptor()] + "PluginName", interceptors=[SomeWorkerInterceptor(), SomeClientInterceptor()] ) ``` + {/* SNIPEND */} + + + {/* SNIPSTART go-plugin-interceptors */} +[features/snippets/plugins/plugins.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.go) +```go +type SomeWorkerInterceptor struct { + interceptor.WorkerInterceptorBase +} + +type SomeClientInterceptor struct { + interceptor.ClientInterceptorBase +} + +func createInterceptorPlugin() (*temporal.SimplePlugin, error) { + return temporal.NewSimplePlugin(temporal.SimplePluginOptions{ + Name: "PluginName", + WorkerInterceptors: []interceptor.WorkerInterceptor{&SomeWorkerInterceptor{}}, + ClientInterceptors: []interceptor.ClientInterceptor{&SomeClientInterceptor{}}, + }) +} + +``` + {/* SNIPEND */} + + + {/* SNIPSTART java-plugin-interceptors */} +[features/snippets/plugins/plugins.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.java) +```java +public class SomeWorkerInterceptor extends WorkerInterceptorBase { + // Your worker interceptor implementation +} + +public class SomeClientInterceptor extends WorkflowClientInterceptorBase { + // Your client interceptor implementation +} + +SimplePlugin interceptorPlugin = + SimplePlugin.newBuilder("PluginName") + .addWorkerInterceptors(new SomeWorkerInterceptor()) + .addClientInterceptors(new SomeClientInterceptor()) + .build(); +``` + {/* SNIPEND */} + + + {/* SNIPSTART typescript-plugins-interceptors */} +[features/snippets/plugins/plugins.ts](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.ts) +```ts +class MyWorkflowClientInterceptor implements WorkflowClientInterceptor {} + +class MyActivityInboundInterceptor implements ActivityInboundCallsInterceptor {} + +class MyActivityOutboundInterceptor implements ActivityOutboundCallsInterceptor {} + +const workflowInterceptorsPath = ''; + +const plugin = new SimplePlugin({ + name: 'plugin-name', + clientInterceptors: { + workflow: [new MyWorkflowClientInterceptor()], + }, + workerInterceptors: { + client: { + workflow: [new MyWorkflowClientInterceptor()], + }, + workflowModules: [workflowInterceptorsPath], + activity: [ + (_: Context) => ({ + inbound: new MyActivityInboundInterceptor(), + outbound: new MyActivityOutboundInterceptor(), + }), + ], + }, +}); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-plugins-interceptors */} +[features/snippets/plugins/plugins.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.cs) +```cs +private class SomeClientInterceptor : IClientInterceptor +{ + public ClientOutboundInterceptor InterceptClient( + ClientOutboundInterceptor nextInterceptor) => + throw new NotImplementedException(); +} + +private class SomeWorkerInterceptor : IWorkerInterceptor +{ + public WorkflowInboundInterceptor InterceptWorkflow( + WorkflowInboundInterceptor nextInterceptor) => + throw new NotImplementedException(); + + public ActivityInboundInterceptor InterceptActivity( + ActivityInboundInterceptor nextInterceptor) => + throw new NotImplementedException(); +} + +SimplePlugin interceptorPlugin = new SimplePlugin( + "PluginName", + new SimplePluginOptions() + { + ClientInterceptors = new List() { new SomeClientInterceptor() }, + WorkerInterceptors = new List() { new SomeWorkerInterceptor() }, + }); +``` + {/* SNIPEND */} + + + {/* SNIPSTART ruby-plugin-interceptors */} +[features/snippets/plugins/plugins.rb](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.rb) +```rb +class SomeWorkerInterceptor + include Temporalio::Worker::Interceptor::Workflow + + def intercept_workflow(next_interceptor) + # Your interceptor implementation + next_interceptor + end +end + +class SomeClientInterceptor + include Temporalio::Client::Interceptor + + def intercept_client(next_interceptor) + # Your interceptor implementation + next_interceptor + end +end + +plugin = Temporalio::SimplePlugin.new( + name: 'PluginName', + client_interceptors: [SomeClientInterceptor.new], + worker_interceptors: [SomeWorkerInterceptor.new] +) +``` + {/* SNIPEND */} + + ### Special considerations for different languages Each of the SDKs has nuances you should be aware of so you can account for it in your code. -For example, you can choose to [run your Workflows in a sandbox in Python](/develop/python/python-sdk-sandbox). This lets you run Workflow code in a sandbox environment to help prevent non-determinism errors in your application. To work for users who use sandboxing, your Plugin should specify the Workflow runner that it uses. +#### Python +You can choose to [run your Workflows in a sandbox in Python](/develop/python/python-sdk-sandbox). This lets you run Workflow code in a sandbox environment to help prevent non-determinism errors in your application. To work for users who use sandboxing, your Plugin should specify the Workflow runner that it uses. -Here's an example of how to explicitly define the Workflow runner for your Plugin with Python: +{/* SNIPSTART python-plugin-sandbox */} +[features/snippets/plugins/plugins.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/plugins/plugins.py) +```py +def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the plugin.") -```python -def workflow_runner(runner: Optional[WorkflowRunner]) -> WorkflowRunner: - if not runner: - raise ValueError("No WorkflowRunner provided to the OpenAI plugin.") + # If in sandbox, add additional passthrough + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules("module"), + ) + return runner - # If in sandbox, add additional passthrough - if isinstance(runner, SandboxedWorkflowRunner): - return dataclasses.replace( - runner, - restrictions=runner.restrictions.with_passthrough_modules("mcp"), - ) - return runner -SimplePlugin(..., workflow_runner=workflow_runner) +plugin = SimplePlugin("PluginName", workflow_runner=workflow_runner) ``` +{/* SNIPEND */} ## Testing your Plugin {#testing-your-plugin} @@ -256,112 +807,76 @@ When you make changes to your plugin after it has already shipped to users, it's Your Plugin should cater to Workflows resuming in different processes than the ones they started on and then replaying from the beginning, which can happen, for example, after an intermittent failure. -You can ensure you're not depending on local side effects by turning Workflow caching off, which will mean that the Workflow replays from the top each time it progresses. Here's an example with Python: +You can ensure you're not depending on local side effects by turning Workflow caching off, which will mean that the Workflow replays from the top each time it progresses: -```python -import my_module - -client = await Client.connect( - "localhost:7233", - plugins=[ - my_module.my_plugin, - ], + + + {/* SNIPSTART python-worker-max-cached-workflows */} +[features/snippets/worker/worker.py](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.py) +```py +worker = Worker(client, task_queue="task-queue", max_cached_workflows=0) +``` + {/* SNIPEND */} + + + {/* SNIPSTART go-worker-max-cached-workflows */} +[features/snippets/worker/worker.go](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.go) +```go +worker.SetStickyWorkflowCacheSize(0) +w := worker.New(c, "task-queue", worker.Options{}) +``` + {/* SNIPEND */} + + + {/* SNIPSTART java-worker-max-cached-workflows */} +[features/snippets/worker/worker.java](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.java) +```java +WorkerFactory factory = + WorkerFactory.newInstance( + client, WorkerFactoryOptions.newBuilder().setWorkflowCacheSize(0).build()); +Worker worker = factory.newWorker("task-queue"); +``` + {/* SNIPEND */} + + + {/* SNIPSTART typescript-worker-max-cached-workflows */} +[features/snippets/worker/worker.ts](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.ts) +```ts +const worker = await Worker.create({ + connection, + taskQueue: 'task-queue', + maxCachedWorkflows: 0, +}); +``` + {/* SNIPEND */} + + + {/* SNIPSTART dotnet-worker-max-cached-workflows */} +[features/snippets/worker/worker.cs](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.cs) +```cs +using var worker = new TemporalWorker( + client, + new TemporalWorkerOptions("task-queue") + { + MaxCachedWorkflows = 0 + }); +``` + {/* SNIPEND */} + + + {/* SNIPSTART ruby-worker-max-cached-workflows */} +[features/snippets/worker/worker.rb](https://github.com/temporalio/features/blob/plugin_snippets/features/snippets/worker/worker.rb) +```rb +worker = Temporalio::Worker.new( + client: client, + task_queue: 'task-queue', + max_cached_workflows: 0 ) -async with Worker( - client, - task_queue="task-queue", - max_cached_workflows=0 -): - # Start a workflow ... ``` + {/* SNIPEND */} + + Check for duplicate side effects or other types of failures. It's harder to test against side effects to global variables, so this practice is best avoided entirely. - -## Advanced Topics for Plugins - -If you go deeper into `SimplePlugin`, you'll see it aggregates a pair of raw Plugin classes that you can use for a higher level of flexibility: a Worker Plugin and a client Plugin. - -- Worker Plugins contain functionality that runs inside your users’ Workflows. -- Client Plugins contain functionality that runs when Workflows are created and return results. - -If your Plugin implements both of them, registering it in the client will also register it in Workers created with that client. - -### Client Plugin - -Client Plugins are provided to the Temporal client on creation. They can change client configurations and service client configurations. `ClientConfig` contains settings like client Interceptors and DataConverters. `ConnectConfig` configures the actual network connections to the local or cloud Temporal server with values like an API key. This is the basic implementation of a client Plugin using Python: - -```python -class MyAdvancedClientPlugin(temporalio.client.Plugin): - - def configure_client(self, config: ClientConfig) -> ClientConfig: - return config - - async def connect_service_client( - self, - config: ConnectConfig, - next: Callable[[ConnectConfig], Awaitable[ServiceClient]], - ) -> temporalio.service.ServiceClient: - return await next(config) -``` - -The primary use case for integrations so far is setting a `DataConverter`, like in the [Data Converter example](#custom-data-converters). - -### Worker Plugin - -Worker Plugins are provided at Worker creation and have more capabilities and corresponding implementation than client Plugins. They can change Worker configurations, run code during the Worker lifetime, and manage the Replayer in a similar way. You can learn more about the [Replayer](#replayer) in a later section. - -Similar to `configure_client` above, you implement `configure_worker` and `configure_replayer` to change any necessary configurations. In addition, `run_worker` allows you to execute code before and after the Worker runs. This can be used to set up resources or globals for use during the Worker execution. `run_replayer` does the same for the Replayer, but keep in mind that the Replayer has a more complex return type. This is a basic implementation of a Worker plugin using Python: - -```python -class MyAdvancedWorkerPlugin(temporalio.worker.Plugin): - def configure_worker(self, config: WorkerConfig) -> WorkerConfig: - return config - - async def run_worker( - self, worker: Worker, next: Callable[[Worker], Awaitable[None]] - ) -> None: - next(worker) - - def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return config - - def run_replayer( - self, - replayer: Replayer, - histories: AsyncIterator[temporalio.client.WorkflowHistory], - next: Callable[ - [Replayer, AsyncIterator[WorkflowHistory]], - AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], - ], - ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return next(replayer, histories) -``` - -### Replayer - -The Replayer allows Workflow authors to validate that their Workflows will work after changes to either the Workflow or a library they depend on. It’s normally used in test runs or when testing Workers before they roll out in production. - -The Replayer runs on a Workflow History created by a previous Workflow run. Suppose something in the Workflow or underlying code has changed in a way which could potentially cause a non-determinism error. In that case, the Replayer will notice the change in the way it runs compared to the history provided. - -The Replayer is typically configured identically to the Worker and client. Ff you’re using `SimplePlugin`, this is already handled for you. - -If you need to do something custom for the Replayer, you can configure it directly. Here's an example of how to do that with Python: - -```python -class MyAdvancedWorkerPlugin(temporalio.worker.Plugin): - def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return config - - def run_replayer( - self, - replayer: Replayer, - histories: AsyncIterator[temporalio.client.WorkflowHistory], - next: Callable[ - [Replayer, AsyncIterator[WorkflowHistory]], - AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], - ], - ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return next(replayer, histories) -``` diff --git a/snipsync.config.yaml b/snipsync.config.yaml index cffe0de3fb..a5cd3e1bd8 100644 --- a/snipsync.config.yaml +++ b/snipsync.config.yaml @@ -9,6 +9,8 @@ origins: repo: samples-python - owner: temporalio repo: reference-app-orders-go + - owner: temporalio + repo: features # Some samples were left in the SDK code itself because it has better testing # capabilities than the samples repo - owner: temporalio