|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | +package io.durabletask.samples; |
| 4 | + |
| 5 | +import com.microsoft.durabletask.DurableTaskClient; |
| 6 | +import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; |
| 7 | +import com.microsoft.durabletask.DurableTaskGrpcWorker; |
| 8 | +import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; |
| 9 | +import com.microsoft.durabletask.NewOrchestrationInstanceOptions; |
| 10 | +import com.microsoft.durabletask.OrchestrationMetadata; |
| 11 | +import com.microsoft.durabletask.TaskActivity; |
| 12 | +import com.microsoft.durabletask.TaskActivityFactory; |
| 13 | +import com.microsoft.durabletask.TaskOrchestration; |
| 14 | +import com.microsoft.durabletask.TaskOrchestrationFactory; |
| 15 | + |
| 16 | +import org.slf4j.Logger; |
| 17 | +import org.slf4j.LoggerFactory; |
| 18 | + |
| 19 | +import java.io.IOException; |
| 20 | +import java.time.Duration; |
| 21 | +import java.util.concurrent.TimeoutException; |
| 22 | + |
| 23 | +/** |
| 24 | + * Demonstrates the replay-safe logging API on {@code TaskOrchestrationContext}, the Java |
| 25 | + * counterpart of the .NET SDK's {@code ReplaySafeLoggerFactorySample}. |
| 26 | + * |
| 27 | + * <p>An orchestrator replays its history every time it resumes. A normal SLF4J logger therefore |
| 28 | + * emits the same message multiple times — once per replay. A logger obtained from |
| 29 | + * {@link com.microsoft.durabletask.TaskOrchestrationContext#createReplaySafeLogger(Class)} |
| 30 | + * suppresses output during replay and logs only on the first (non-replay) execution. |
| 31 | + * |
| 32 | + * <p>This sample runs an orchestrator that calls two activities. Each activity call causes the |
| 33 | + * orchestrator to yield and replay on resume, so the messages logged via the standard logger |
| 34 | + * appear more than once while the replay-safe logger's messages appear exactly once. |
| 35 | + * |
| 36 | + * <p>Run it like any other sample (requires a Durable Task sidecar / DTS emulator on the default |
| 37 | + * gRPC endpoint): |
| 38 | + * <pre>{@code |
| 39 | + * ./gradlew :samples:runReplaySafeLoggingPattern |
| 40 | + * }</pre> |
| 41 | + * |
| 42 | + * <h3>Wrapper-context pattern (.NET parity)</h3> |
| 43 | + * If you wrap {@code TaskOrchestrationContext} in your own type and want logging emitted from |
| 44 | + * the wrapper to also be replay-safe, override {@code getLoggerFactory()} to return the inner |
| 45 | + * context's {@code getReplaySafeLoggerFactory()}: |
| 46 | + * <pre>{@code |
| 47 | + * final class LoggingContext implements TaskOrchestrationContext { |
| 48 | + * private final TaskOrchestrationContext inner; |
| 49 | + * LoggingContext(TaskOrchestrationContext inner) { this.inner = inner; } |
| 50 | + * |
| 51 | + * @Override |
| 52 | + * public ILoggerFactory getLoggerFactory() { |
| 53 | + * return inner.getReplaySafeLoggerFactory(); // SDK unwraps to avoid double-wrapping |
| 54 | + * } |
| 55 | + * // ... forward all other methods to `inner` ... |
| 56 | + * } |
| 57 | + * }</pre> |
| 58 | + * Implementing the full wrapper requires forwarding every abstract method on the interface, so |
| 59 | + * it is omitted here for brevity. The mechanism is the same as the .NET SDK's |
| 60 | + * {@code protected override ILoggerFactory LoggerFactory => inner.ReplaySafeLoggerFactory;}. |
| 61 | + */ |
| 62 | +final class ReplaySafeLoggingPattern { |
| 63 | + |
| 64 | + private static final String ORCHESTRATION_NAME = "ReplaySafeLoggingOrchestration"; |
| 65 | + private static final String ECHO_ACTIVITY = "Echo"; |
| 66 | + |
| 67 | + public static void main(String[] args) throws IOException, InterruptedException, TimeoutException { |
| 68 | + DurableTaskGrpcWorker worker = createWorker(); |
| 69 | + worker.start(); |
| 70 | + |
| 71 | + try { |
| 72 | + DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); |
| 73 | + |
| 74 | + String instanceId = client.scheduleNewOrchestrationInstance( |
| 75 | + ORCHESTRATION_NAME, |
| 76 | + new NewOrchestrationInstanceOptions().setInput("Seattle")); |
| 77 | + System.out.printf("Started new orchestration instance: %s%n", instanceId); |
| 78 | + |
| 79 | + OrchestrationMetadata completed = client.waitForInstanceCompletion( |
| 80 | + instanceId, |
| 81 | + Duration.ofSeconds(30), |
| 82 | + true); |
| 83 | + |
| 84 | + System.out.printf("Orchestration completed: %s%n", completed.getRuntimeStatus()); |
| 85 | + System.out.printf("Output: %s%n", completed.readOutputAs(String.class)); |
| 86 | + System.out.println(); |
| 87 | + System.out.println( |
| 88 | + "Note: the non-replay-safe logger's messages should appear multiple times " + |
| 89 | + "(once per replay), while the replay-safe logger's messages appear once."); |
| 90 | + } finally { |
| 91 | + worker.stop(); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + private static DurableTaskGrpcWorker createWorker() { |
| 96 | + DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); |
| 97 | + |
| 98 | + builder.addOrchestration(new TaskOrchestrationFactory() { |
| 99 | + @Override |
| 100 | + public String getName() { return ORCHESTRATION_NAME; } |
| 101 | + |
| 102 | + @Override |
| 103 | + public TaskOrchestration create() { |
| 104 | + return ctx -> { |
| 105 | + String input = ctx.getInput(String.class); |
| 106 | + |
| 107 | + // Non-replay-safe logger: emits on every replay. |
| 108 | + Logger plainLogger = LoggerFactory.getLogger(ReplaySafeLoggingPattern.class); |
| 109 | + |
| 110 | + // Replay-safe logger: emits only when the orchestrator is NOT replaying. |
| 111 | + Logger replaySafeLogger = ctx.createReplaySafeLogger(ReplaySafeLoggingPattern.class); |
| 112 | + |
| 113 | + plainLogger.info("[plain] starting orchestration for input='{}'", input); |
| 114 | + replaySafeLogger.info("[replay-safe] starting orchestration for input='{}'", input); |
| 115 | + |
| 116 | + String greeting = ctx.callActivity(ECHO_ACTIVITY, "Hello, " + input + "!", String.class).await(); |
| 117 | + |
| 118 | + plainLogger.info("[plain] first activity returned '{}'", greeting); |
| 119 | + replaySafeLogger.info("[replay-safe] first activity returned '{}'", greeting); |
| 120 | + |
| 121 | + String farewell = ctx.callActivity(ECHO_ACTIVITY, "Goodbye, " + input + "!", String.class).await(); |
| 122 | + |
| 123 | + plainLogger.info("[plain] second activity returned '{}'", farewell); |
| 124 | + replaySafeLogger.info("[replay-safe] second activity returned '{}'", farewell); |
| 125 | + |
| 126 | + ctx.complete(greeting + " / " + farewell); |
| 127 | + }; |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | + builder.addActivity(new TaskActivityFactory() { |
| 132 | + @Override |
| 133 | + public String getName() { return ECHO_ACTIVITY; } |
| 134 | + |
| 135 | + @Override |
| 136 | + public TaskActivity create() { |
| 137 | + return ctx -> ctx.getInput(String.class); |
| 138 | + } |
| 139 | + }); |
| 140 | + return builder.build(); |
| 141 | + } |
| 142 | + |
| 143 | + private ReplaySafeLoggingPattern() { |
| 144 | + // sample entry point |
| 145 | + } |
| 146 | +} |
0 commit comments