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
4 changes: 4 additions & 0 deletions aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### September 2, 2026
`2.12.1`
- Emit a structured `runtime_worker_pool_initializing` DEBUG log event once during INIT in multi-concurrent (Lambda Managed Instances) mode, reporting the worker pool size (`workerCount`) and the maximum concurrency the execution environment supports (`executionEnvironmentMaxConcurrency`). Only visible when the function log level is DEBUG or lower; not emitted for standard on-demand functions.

### July 17, 2026
`2.12.0`
- Add `Lambda-Runtime-Invocation-Id` header support for cross-wiring protection. The RIC now echoes the invocation ID received from RAPID on `/next` back on `/response` and `/error`, enabling RAPID to detect and reject stale responses from timed-out invocations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand Down Expand Up @@ -251,6 +255,14 @@ protected static void startRuntimeLoops(LambdaRequestHandler lambdaRequestHandle
if (concurrencyConfig.isMultiConcurrent()) {
lambdaLogger.log(concurrencyConfig.getConcurrencyConfigMessage(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.INFO : LogLevel.UNDEFINED);
ExecutorService platformThreadExecutor = Executors.newFixedThreadPool(concurrencyConfig.getNumberOfPlatformThreads());
// Emitted once during INIT. The byte[] log path applies level filtering but no
// formatting, so the hand-built line keeps "message" as a nested JSON object.
if (lambdaLogger.getLogFormat() == LogFormat.JSON) {
WorkerPoolInitializedEvent event = new WorkerPoolInitializedEvent(
concurrencyConfig.getNumberOfPlatformThreads(),
concurrencyConfig.getNumberOfPlatformThreads());
lambdaLogger.log(event.toJsonLogLine().getBytes(StandardCharsets.UTF_8), LogLevel.DEBUG);
}
try {
for (int i = 0; i < concurrencyConfig.getNumberOfPlatformThreads(); i++) {
startRuntimeLoopWithExecutor(lambdaRequestHandler, lambdaLogger, platformThreadExecutor, runtimeClient);
Expand Down Expand Up @@ -373,4 +385,30 @@ private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exc
protected static URLClassLoader getCustomerClassLoader() {
return customerClassLoader;
}

static class WorkerPoolInitializedEvent {
static final String EVENT_NAME = "runtime_worker_pool_initializing";
private static final DateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneId.of("UTC"));

final int workerCount;
final int executionEnvironmentMaxConcurrency;

WorkerPoolInitializedEvent(int workerCount, int executionEnvironmentMaxConcurrency) {
this.workerCount = workerCount;
this.executionEnvironmentMaxConcurrency = executionEnvironmentMaxConcurrency;
}

/**
* Complete JSON log line with "message" as a nested object (queryable in CloudWatch
* Logs Insights). Hand-built safely: all values are constants or ints, no escaping needed.
*/
String toJsonLogLine() {
return "{\"timestamp\":\"" + TIMESTAMP_FORMAT.format(Instant.now())
+ "\",\"message\":{\"event\":\"" + EVENT_NAME
+ "\",\"workerCount\":" + workerCount
+ ",\"executionEnvironmentMaxConcurrency\":" + executionEnvironmentMaxConcurrency
+ "},\"level\":\"DEBUG\"}\n";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,82 @@ void testSequentialWithVirtualMachineErrorStopsLoop() throws Throwable {
assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get());
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testWorkerPoolInitializedEventEmittedOnceInMultiConcurrentMode() throws Throwable {
when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON);
when(concurrencyConfig.isMultiConcurrent()).thenReturn(true);
when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4);

when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger))
.thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException);

AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient);

org.mockito.ArgumentCaptor<byte[]> lineCaptor = org.mockito.ArgumentCaptor.forClass(byte[].class);
verify(lambdaLogger, times(1)).log(lineCaptor.capture(), eq(LogLevel.DEBUG));

com.amazonaws.lambda.thirdparty.org.json.JSONObject parsed =
new com.amazonaws.lambda.thirdparty.org.json.JSONObject(
new String(lineCaptor.getValue(), java.nio.charset.StandardCharsets.UTF_8));
com.amazonaws.lambda.thirdparty.org.json.JSONObject message = parsed.getJSONObject("message");
assertEquals("runtime_worker_pool_initializing", message.getString("event"));
assertEquals(4, message.getInt("workerCount"));
assertEquals(4, message.getInt("executionEnvironmentMaxConcurrency"));
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testWorkerPoolInitializedEventNotEmittedInNonJsonLogFormat() throws Throwable {
// Guard: no emission when the log format is not JSON, even in multi-concurrent mode
when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.TEXT);
when(concurrencyConfig.isMultiConcurrent()).thenReturn(true);
when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(2);

when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger))
.thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException);

AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient);

verify(lambdaLogger, never()).log(any(byte[].class), any(LogLevel.class));
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testWorkerPoolInitializedEventNotEmittedInSequentialMode() throws Throwable {
when(concurrencyConfig.isMultiConcurrent()).thenReturn(false);

InvocationRequest fatalRequest = mock(InvocationRequest.class);
when(fatalRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn("fatal");
when(runtimeClient.nextInvocation()).thenReturn(fatalRequest);

AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient);

verify(lambdaLogger, never()).log(any(byte[].class), any(LogLevel.class));
}

/*
* Pins the exact wire format of the hand-built log line: "message" must be a nested
* JSON object with exactly the documented schema, inside a valid envelope.
*/
@Test
void testWorkerPoolInitializedEventJsonWireFormat() {
String line = new AWSLambda.WorkerPoolInitializedEvent(16, 16).toJsonLogLine();
org.junit.jupiter.api.Assertions.assertTrue(line.endsWith("\n"));

com.amazonaws.lambda.thirdparty.org.json.JSONObject parsed =
new com.amazonaws.lambda.thirdparty.org.json.JSONObject(line);
assertEquals("DEBUG", parsed.getString("level"));
org.junit.jupiter.api.Assertions.assertNotNull(parsed.getString("timestamp"));
assertEquals(3, parsed.length());

com.amazonaws.lambda.thirdparty.org.json.JSONObject message = parsed.getJSONObject("message");
assertEquals("runtime_worker_pool_initializing", message.getString("event"));
assertEquals(16, message.getInt("workerCount"));
assertEquals(16, message.getInt("executionEnvironmentMaxConcurrency"));
assertEquals(3, message.length());
}

@Test
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testInvocationIdIsPassedToReportSuccess() throws Throwable {
Expand Down
Loading