Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Unreleased
* Add `createReplaySafeLogger` to suppress orchestration log output during replay.
* Add `getParentInstance()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))

## v1.9.0
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ result += ctx.callActivity("SayHello", "Seattle", String.class).await();
return result;
```

### Replay-safe orchestration logging

Orchestrator code re-executes while rebuilding state from history. Wrap an existing
`java.util.logging.Logger` to suppress log output during those replay segments:

```java
Logger logger = ctx.createReplaySafeLogger(
Logger.getLogger(MyOrchestration.class.getName()));

logger.info("Starting orchestration " + ctx.getInstanceId());
String result = ctx.callActivity("ProcessItem", input, String.class).await();
logger.info(() -> "Activity returned: " + result);
```

In Azure Functions, pass `ExecutionContext.getLogger()` instead of creating a named
logger so the output retains its invocation ID and normal host routing:

```java
Logger logger = ctx.createReplaySafeLogger(executionContext.getLogger());
```

Replay-safe logging suppresses calls made while replaying; it does not guarantee
exactly-once log delivery across failed or retried live orchestration turns.

### Reliable fan-out / fan-in orchestration pattern

```java
Expand Down
300 changes: 300 additions & 0 deletions client/src/main/java/com/microsoft/durabletask/ReplaySafeLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.durabletask;

import java.util.Objects;
import java.util.ResourceBundle;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import java.util.logging.Filter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

final class ReplaySafeLogger extends Logger {
private final Logger delegate;
private final BooleanSupplier isReplaying;

ReplaySafeLogger(Logger delegate, BooleanSupplier isReplaying) {
super(Objects.requireNonNull(delegate, "delegate").getName(), null);
this.delegate = delegate;
this.isReplaying = Objects.requireNonNull(isReplaying, "isReplaying");
}

@Override
public boolean isLoggable(Level level) {
return this.delegate.isLoggable(level);
}

@Override
public void log(LogRecord record) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(record);
}
}

@Override
public void log(Level level, String message) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, message);
}
}

@Override
public void log(Level level, Supplier<String> messageSupplier) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, messageSupplier);
}
}

@Override
public void log(Level level, String message, Object parameter) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, message, parameter);
}
}

@Override
public void log(Level level, String message, Object[] parameters) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, message, parameters);
}
}

@Override
public void log(Level level, String message, Throwable thrown) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, message, thrown);
}
}

@Override
public void log(Level level, Throwable thrown, Supplier<String> messageSupplier) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.log(level, thrown, messageSupplier);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
String message) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, message);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
Supplier<String> messageSupplier) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, messageSupplier);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
String message,
Object parameter) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, message, parameter);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
String message,
Object[] parameters) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, message, parameters);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
String message,
Throwable thrown) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, message, thrown);
}
}

@Override
public void logp(
Level level,
String sourceClass,
String sourceMethod,
Throwable thrown,
Supplier<String> messageSupplier) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logp(level, sourceClass, sourceMethod, thrown, messageSupplier);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
String bundleName,
String message) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
String bundleName,
String message,
Object parameter) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, parameter);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
String bundleName,
String message,
Object[] parameters) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, parameters);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
ResourceBundle bundle,
String message,
Object... parameters) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundle, message, parameters);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
String bundleName,
String message,
Throwable thrown) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, thrown);
}
}

@Override
public void logrb(
Level level,
String sourceClass,
String sourceMethod,
ResourceBundle bundle,
String message,
Throwable thrown) {
if (!this.isReplaying.getAsBoolean()) {
this.delegate.logrb(level, sourceClass, sourceMethod, bundle, message, thrown);
}
}

@Override
public String getName() {
return this.delegate.getName();
}

@Override
public ResourceBundle getResourceBundle() {
return this.delegate.getResourceBundle();
}

@Override
public String getResourceBundleName() {
return this.delegate.getResourceBundleName();
}

@Override
public void setResourceBundle(ResourceBundle bundle) {
this.delegate.setResourceBundle(bundle);
}

@Override
public Filter getFilter() {
return this.delegate.getFilter();
}

@Override
public void setFilter(Filter filter) {
this.delegate.setFilter(filter);
}

@Override
public Level getLevel() {
return this.delegate.getLevel();
}

@Override
public void setLevel(Level level) {
this.delegate.setLevel(level);
}

@Override
public Handler[] getHandlers() {
return this.delegate.getHandlers();
}

@Override
public void addHandler(Handler handler) {
this.delegate.addHandler(handler);
}

@Override
public void removeHandler(Handler handler) {
this.delegate.removeHandler(handler);
}

@Override
public Logger getParent() {
return this.delegate.getParent();
}

@Override
public void setParent(Logger parent) {
this.delegate.setParent(parent);
}

@Override
public boolean getUseParentHandlers() {
return this.delegate.getUseParentHandlers();
}

@Override
public void setUseParentHandlers(boolean useParentHandlers) {
this.delegate.setUseParentHandlers(useParentHandlers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Logger;
import javax.annotation.Nonnull;

/**
Expand Down Expand Up @@ -62,6 +64,28 @@ public interface TaskOrchestrationContext {
*/
boolean getIsReplaying();

/**
* Creates a logger that suppresses output while this orchestration is replaying.
* <p>
* The returned logger does not own {@code logger} or any of its handlers. Replay-safe logging suppresses replay
* output but does not guarantee exactly-once delivery across failed or retried live orchestration turns.
* <p>
* {@link Logger#isLoggable} continues to report the supplied logger's level state during replay. Use
* supplier-based logging methods to avoid expensive message construction while replaying.
* <p>
* Automatic source-class and source-method inference is not preserved by all JUL convenience methods. Use
* {@link Logger#logp} when explicit source metadata is required.
*
* @param logger the configured logger to wrap
* @return a logger that emits through {@code logger} only when not replaying
* @throws NullPointerException if {@code logger} is {@code null}
*/
default Logger createReplaySafeLogger(Logger logger) {
return new ReplaySafeLogger(
Objects.requireNonNull(logger, "logger"),
this::getIsReplaying);
}

/**
* Gets the version of the orchestration that this context represents.
*
Expand Down
Loading
Loading