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
33 changes: 20 additions & 13 deletions .github/workflows/conformance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ name: Conformance Tests

# Full-integration conformance run: builds the SDK + Java handlers, installs the
# pinned language-agnostic runner from PyPI, then deploys + invokes + validates
# one SAM stack per suite. Each suite runs as its own parallel matrix job. To add
# a suite later, add its name to `strategy.matrix.suite` (and ship its
# template_<suite>.yaml + handlers under conformance-tests/).
# one SAM stack per suite. Suites are discovered from the module's
# template_<suite>.yaml files (see conformance-tests/scripts/discover_suites.py)
# and each runs as its own parallel matrix job, so adding a suite only requires
# shipping its template + handlers under conformance-tests/.

on:
pull_request:
Expand All @@ -28,24 +29,30 @@ permissions:
id-token: write # Required for AWS OIDC credentials

jobs:
discover_suites:
name: discover conformance suites
runs-on: ubuntu-latest
outputs:
suites: ${{ steps.discover.outputs.suites }}
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Discover suites from templates
id: discover
working-directory: conformance-tests
run: echo "suites=$(python3 scripts/discover_suites.py)" >> "$GITHUB_OUTPUT"

conformance:
name: conformance (${{ matrix.suite }})
needs: discover_suites
runs-on: ubuntu-latest
env:
AWS_REGION: us-west-2
strategy:
fail-fast: false
matrix:
suite:
- step
- wait
- child
- callback
- invoke
- parallel
- wait_for_callback
- wait_for_condition
- map
suite: ${{ fromJSON(needs.discover_suites.outputs.suites) }}
defaults:
run:
working-directory: conformance-tests
Expand Down
47 changes: 47 additions & 0 deletions conformance-tests/scripts/discover_suites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Discover conformance suites from template files."""

from __future__ import annotations

import json
from pathlib import Path


MODULE_DIR = Path(__file__).resolve().parents[1]
TEMPLATE_PREFIX = "template_"
TEMPLATE_SUFFIX = ".yaml"
SOURCE_ROOT = Path("src") / "main" / "java"


def discover_suites(module_dir: Path = MODULE_DIR) -> tuple[str, ...]:
"""Return sorted suites with matching templates and non-empty handler packages."""
templates = sorted(module_dir.glob(f"{TEMPLATE_PREFIX}*{TEMPLATE_SUFFIX}"))
if not templates:
raise SystemExit(f"No {TEMPLATE_PREFIX}<suite>{TEMPLATE_SUFFIX} files found")

suites: list[str] = []
for template in templates:
suite = template.name[len(TEMPLATE_PREFIX) : -len(TEMPLATE_SUFFIX)]
if not suite:
raise SystemExit(f"Invalid conformance template name: {template.name}")

handlers_dir = module_dir / SOURCE_ROOT / suite
if not handlers_dir.is_dir():
raise SystemExit(
f"Template {template.name} has no matching handler package: {handlers_dir}"
)

if not list(handlers_dir.glob("*.java")):
raise SystemExit(f"No handler classes found for suite {suite}: {handlers_dir}")

suites.append(suite)

return tuple(suites)


def main() -> None:
print(json.dumps(discover_suites(), separators=(",", ":")))


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plugin;

import software.amazon.lambda.durable.plugin.DurableExecutionPlugin;
import software.amazon.lambda.durable.plugin.InvocationEndInfo;
import software.amazon.lambda.durable.plugin.InvocationInfo;
import software.amazon.lambda.durable.plugin.OperationEndInfo;
import software.amazon.lambda.durable.plugin.OperationInfo;
import software.amazon.lambda.durable.plugin.UserFunctionEndInfo;
import software.amazon.lambda.durable.plugin.UserFunctionStartInfo;

/**
* Shared instrumentation plugin for the plugin conformance suite.
*
* <p>Emits lifecycle log lines with a configurable prefix (e.g. {@code CONFPLUGIN}, {@code CONFPLUGIN-A}) so one
* plugin — or two, for the multiple-plugins case — can be registered on a handler. Operation- and attempt-level hooks
* are filtered to step-type operations to match the requirement vocabulary. All lines are emitted from the real SDK
* plugin hooks; nothing is hand-rolled.
*/
@SuppressWarnings("deprecation")
public class ConformanceLoggingPlugin implements DurableExecutionPlugin {

private final String prefix;

/** Captured from onInvocationStart; read by later hooks that may run on other threads. */
private volatile String executionArn;

public ConformanceLoggingPlugin(String prefix) {
this.prefix = prefix;
}

private static boolean isStep(String type) {
return "STEP".equals(type);
}

/** Returns {@code , "durableExecutionArn": "<arn>"} when captured, otherwise an empty string. */
private String arnField() {
return executionArn == null ? "" : String.format(", \"durableExecutionArn\": \"%s\"", executionArn);
}

@Override
public void onInvocationStart(InvocationInfo info) {
this.executionArn = info.durableExecutionArn();
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"invocation-start\", \"first\": %b%s}",
prefix, info.isFirstInvocation(), arnField()));
}

@Override
public void onInvocationEnd(InvocationEndInfo info) {
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"%s}",
prefix, info.invocationStatus().name(), arnField()));
}

@Override
public void onOperationStart(OperationInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"operation-start\", \"op\": \"%s\"%s}",
prefix, info.id(), arnField()));
}
}

@Override
public void onOperationEnd(OperationEndInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"operation-end\", \"op\": \"%s\", \"status\": \"%s\"%s}",
prefix, info.id(), info.status(), arnField()));
}
}

@Override
public void onUserFunctionStart(UserFunctionStartInfo info) {
if (isStep(info.type()) && info.attempt() != null) {
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"attempt-start\", \"n\": %d, \"op\": \"%s\"%s}",
prefix, info.attempt(), info.id(), arnField()));
}
}

@Override
public void onUserFunctionEnd(UserFunctionEndInfo info) {
if (isStep(info.type()) && info.attempt() != null) {
String outcome = info.succeeded() ? "SUCCEEDED" : "FAILED";
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"attempt-end\", \"n\": %d, \"outcome\": \"%s\", \"op\": \"%s\"%s}",
prefix, info.attempt(), outcome, info.id(), arnField()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plugin;

import software.amazon.lambda.durable.plugin.DurableExecutionPlugin;
import software.amazon.lambda.durable.plugin.InvocationEndInfo;
import software.amazon.lambda.durable.plugin.InvocationInfo;
import software.amazon.lambda.durable.plugin.OperationEndInfo;
import software.amazon.lambda.durable.plugin.OperationInfo;
import software.amazon.lambda.durable.plugin.UserFunctionEndInfo;
import software.amazon.lambda.durable.plugin.UserFunctionStartInfo;

/**
* Instrumentation plugin whose every hook logs a line and then throws.
*
* <p>Used by requirement 10-4 to verify the SDK isolates plugin exceptions: each hook must run (log its line) and the
* thrown exception must be swallowed by the SDK so the execution result and history are identical to running without
* the plugin. Operation- and attempt-level hooks are filtered to step-type operations.
*/
@SuppressWarnings("deprecation")
public class FaultyConformancePlugin implements DurableExecutionPlugin {

private static boolean isStep(String type) {
return "STEP".equals(type);
}

/** Captured from onInvocationStart (before the throw); read by later hooks that may run on other threads. */
private volatile String executionArn;

/** Returns {@code , "durableExecutionArn": "<arn>"} when captured, otherwise an empty string. */
private String arnField() {
return executionArn == null ? "" : String.format(", \"durableExecutionArn\": \"%s\"", executionArn);
}

@Override
public void onInvocationStart(InvocationInfo info) {
this.executionArn = info.durableExecutionArn();
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"%s}", arnField()));
throw new RuntimeException("faulty invocation-start");
}

@Override
public void onInvocationEnd(InvocationEndInfo info) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"%s}", arnField()));
throw new RuntimeException("faulty invocation-end");
}

@Override
public void onOperationStart(OperationInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"%s}", arnField()));
throw new RuntimeException("faulty operation-start");
}
}

@Override
public void onOperationEnd(OperationEndInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"%s}", arnField()));
throw new RuntimeException("faulty operation-end");
}
}

@Override
public void onUserFunctionStart(UserFunctionStartInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"%s}", arnField()));
throw new RuntimeException("faulty attempt-start");
}
}

@Override
public void onUserFunctionEnd(UserFunctionEndInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"%s}", arnField()));
throw new RuntimeException("faulty attempt-end");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plugin;

import java.time.Duration;
import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.retry.RetryDecision;

/**
* 10-3: Plugin attempt hooks fire per step attempt with attempt number and outcome.
*
* <p>A single step that fails on the first attempt and succeeds on the second (driven by the SDK's built-in
* {@code getAttempt()} and a real retry strategy), configured with {@link ConformanceLoggingPlugin}. The plugin logs
* {@code attempt-start n=<n>} / {@code attempt-end n=<n> outcome=<SUCCEEDED|FAILED>} from the user-function hooks,
* which run on the same thread as the step body so their order is deterministic.
*/
@SuppressWarnings("deprecation")
public class PluginAttemptHooksRetry extends DurableHandler<Object, String> {

@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder()
.withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN"))
.build();
}

@Override
public String handleRequest(Object input, DurableContext context) {
return context.step(
"retry-step",
String.class,
stepCtx -> {
// Fail on the first attempt, succeed on the second, using the SDK's
// built-in 1-based attempt number.
if (stepCtx.getAttempt() < 2) {
throw new RuntimeException("Attempt " + stepCtx.getAttempt() + " failed");
}
return "Operation succeeded";
},
StepConfig.builder()
.retryStrategy((error, attempt) -> {
if (attempt >= 3) return RetryDecision.fail();
return RetryDecision.retry(Duration.ofSeconds(1));
})
.build());
}
}
28 changes: 28 additions & 0 deletions conformance-tests/src/main/java/plugin/PluginErrorIsolation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plugin;

import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

/**
* 10-4: Plugin exceptions are swallowed and never affect the execution outcome.
*
* <p>A single greeting step configured with {@link FaultyConformancePlugin}, whose every hook logs a line and then
* throws. The SDK must catch and ignore every plugin exception so the execution result and history are identical to
* running without the plugin.
*/
@SuppressWarnings("deprecation")
public class PluginErrorIsolation extends DurableHandler<String, String> {

@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(new FaultyConformancePlugin()).build();
}

@Override
public String handleRequest(String input, DurableContext context) {
return context.step("greet", String.class, stepCtx -> "Hello, " + input + "!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plugin;

import java.time.Duration;
import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

/**
* 10-6: Plugin sees is-first-invocation true once, then false on replay.
*
* <p>A single 2-second wait configured with {@link ConformanceLoggingPlugin}. The first invocation reports
* {@code first=true} (then suspends), and the replay invocation reports {@code first=false} and finalizes with
* {@code status=SUCCEEDED}.
*/
@SuppressWarnings("deprecation")
public class PluginFirstInvocationFlag extends DurableHandler<Object, String> {

@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder()
.withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN"))
.build();
}

@Override
public String handleRequest(Object input, DurableContext context) {
context.wait(null, Duration.ofSeconds(2));
return "Wait completed";
}
}
Loading