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
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public final class ConfigDefaults {

static final int DEFAULT_CLOCK_SYNC_PERIOD = 30; // seconds

static final boolean DEFAULT_TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED = false;

static final TracePropagationBehaviorExtract DEFAULT_TRACE_PROPAGATION_BEHAVIOR_EXTRACT =
TracePropagationBehaviorExtract.CONTINUE;
static final boolean DEFAULT_TRACE_PROPAGATION_EXTRACT_FIRST = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ public final class TracerConfig {

public static final String CLOCK_SYNC_PERIOD = "trace.clock.sync.period";

/**
* Opt-in (defaults to disabled). When enabled, resyncs the tracer's cached clock reference on
* every AWS Lambda invocation, before any span for that invocation is created, instead of
* relying on the periodic {@link #CLOCK_SYNC_PERIOD} check - which is gated on monotonic ticks
* elapsed and may never trigger following an AWS Lambda SnapStart restore, since a short-lived
* restore-then-invoke sequence can leave that reference stale for hours or days. Only takes
* effect inside an instrumented AWS Lambda handler invocation; a no-op everywhere else.
*/
public static final String TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED =
"trace.lambda.snapstart.clock.resync.enabled";

public static final String TRACE_SPAN_ATTRIBUTE_SCHEMA = "trace.span.attribute.schema";

public static final String TRACE_LONG_RUNNING_ENABLED = "trace.experimental.long-running.enabled";
Expand Down
46 changes: 46 additions & 0 deletions dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,50 @@ long getTimeWithNanoTicks(long nanoTicks) {
return computedNanoTime + counterDrift;
}

/**
* AWS Lambda SnapStart checkpoints the JVM at snapshot-creation time and later restores it,
* potentially much later (a restored snapshot can go {@code Inactive} and be restored again after
* 14 days of no invocations). {@link System#nanoTime()} does not account for the frozen duration
* across that restore, so {@link #startTimeNano}/{@link #startNanoTicks} - captured once at
* construction time, i.e. at snapshot-creation time - go stale, and the periodic self-correction
* in {@link #getTimeWithNanoTicks} may never trigger to fix it: that correction is gated on
* {@code nanoTicks - lastSyncTicks >= clockSyncPeriod} (monotonic ticks elapsed), which a
* short-lived restore-then-invoke sequence can easily never reach even though wall-clock time has
* moved on by hours or days. Left uncorrected, every span timestamp computed via {@link
* #getTimeWithNanoTicks} stays anchored near the original snapshot-creation instant instead of
* the real restore/invocation time.
*
* <p>Rather than reset {@link #startTimeNano}/{@link #startNanoTicks} themselves - which would
* require making those fields {@code volatile}, since they're read on every {@link
* #getTimeWithNanoTicks} call from arbitrary tracing threads - this folds the entire observed
* drift into {@link #counterDrift} directly, the same field (already {@code volatile}) that the
* periodic self-correction in {@link #getTimeWithNanoTicks} uses, just without that correction's
* 1ms drift threshold, since here the drift can be hours or days.
*
* <p>Rather than reacting to the restore event itself (which would need a JVM-level
* checkpoint/restore hook), this is called from {@link #notifyLambdaStart} - already invoked once
* per Lambda invocation, before any span for that invocation is created. Any real restore is
* always followed by an invocation, so resyncing there catches it with no extra dependency. Doing
* this on every invocation (not just ones following a restore) is deliberate: outside SnapStart,
* {@link System#nanoTime()} correctly tracks elapsed time across a warm container's normal
* freeze/thaw between invocations (same continuously-executing process, unlike SnapStart's
* restore-into-a-new-context), so this is a correct no-op there - just a couple of field reads
* and a subtraction, negligible next to the HTTP round-trip {@link #notifyLambdaStart} already
* makes.
*
* <p>Gated on {@link Config#isLambdaSnapStartClockResyncEnabled()} as an escape hatch.
*/
@VisibleForTesting
void maybeResyncClockForLambdaInvocation() {
if (!initialConfig.isLambdaSnapStartClockResyncEnabled()) {
return;
}
long nanoTicks = timeSource.getNanoTicks();
long computedNanoTime = startTimeNano + Math.max(0, nanoTicks - startNanoTicks);
counterDrift = timeSource.getCurrentTimeNanos() - computedNanoTime;
lastSyncTicks = nanoTicks;
}

@Override
public CoreSpanBuilder buildSpan(
final String instrumentationName, final CharSequence operationName) {
Expand Down Expand Up @@ -1238,6 +1282,8 @@ public void closeActive() {

@Override
public AgentSpanContext notifyLambdaStart(Object event, String lambdaRequestId) {
maybeResyncClockForLambdaInvocation();

// Get context from AppSec
AgentSpanContext appSecContext = LambdaAppSecHandler.processRequestStart(event);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import datadog.trace.api.config.TracerConfig;
import datadog.trace.api.remoteconfig.ServiceNameCollector;
import datadog.trace.api.sampling.PrioritySampling;
import datadog.trace.api.time.ControllableTimeSource;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources;
import datadog.trace.common.sampling.AllSampler;
Expand Down Expand Up @@ -79,6 +80,94 @@ void verifyDefaultsOnTracer() {
}
}

@Test
void
getTimeWithNanoTicks_whenNanoTicksStaleAfterSimulatedRestore_thenTimestampStaysAnchoredToConstructionTime() {
// Characterizes the AWS Lambda SnapStart bug this fix addresses: System.nanoTime() does not
// account for the frozen duration across a checkpoint/restore, so a nanoTicks reading taken
// right after restore can be indistinguishable from one taken at construction (snapshot
// creation) time, even though wall-clock time has moved on by hours. Without a resync, span
// timestamps computed from that stale nanoTicks stay anchored to construction time.
ControllableTimeSource timeSource = new ControllableTimeSource();
timeSource.set(TimeUnit.SECONDS.toNanos(1000));
CoreTracer tracer = tracerBuilder().writer(new ListWriter()).timeSource(timeSource).build();
try {
long constructionTimeNanoTicks = timeSource.getNanoTicks();

// Wall-clock time moves on by 2 hours (the restore happens much later), but the nanoTicks
// value a span would be timestamped with right after restore hasn't advanced.
timeSource.set(TimeUnit.SECONDS.toNanos(1000) + TimeUnit.HOURS.toNanos(2));

assertEquals(
TimeUnit.SECONDS.toNanos(1000), tracer.getTimeWithNanoTicks(constructionTimeNanoTicks));
} finally {
tracer.close();
}
}

@Test
@WithConfig(key = TracerConfig.TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED, value = "true")
void
maybeResyncClockForLambdaInvocation_whenEnabledAndCalledAfterSimulatedRestore_thenTimestampReflectsPostRestoreTime() {
ControllableTimeSource timeSource = new ControllableTimeSource();
timeSource.set(TimeUnit.SECONDS.toNanos(1000));
CoreTracer tracer = tracerBuilder().writer(new ListWriter()).timeSource(timeSource).build();
try {
// The restore happens: wall-clock/tick source jumps forward by 2 hours.
long postRestoreNanos = TimeUnit.SECONDS.toNanos(1000) + TimeUnit.HOURS.toNanos(2);
timeSource.set(postRestoreNanos);

tracer.maybeResyncClockForLambdaInvocation();

// A span timestamped right after resync now reflects the real post-restore time, not the
// stale construction-time (pre-restore) anchor.
assertEquals(postRestoreNanos, tracer.getTimeWithNanoTicks(timeSource.getNanoTicks()));
} finally {
tracer.close();
}
}

@Test
@WithConfig(key = TracerConfig.TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED, value = "true")
void notifyLambdaStart_whenExplicitlyEnabled_thenResyncsClockToCurrentTime() {
// notifyLambdaStart runs once per Lambda invocation, before any span for that invocation is
// created - the actual trigger point for the resync in production, not just the extracted
// maybeResyncClockForLambdaInvocation() logic exercised directly above.
ControllableTimeSource timeSource = new ControllableTimeSource();
timeSource.set(TimeUnit.SECONDS.toNanos(1000));
CoreTracer tracer = tracerBuilder().writer(new ListWriter()).timeSource(timeSource).build();
try {
long postRestoreNanos = TimeUnit.SECONDS.toNanos(1000) + TimeUnit.HOURS.toNanos(2);
timeSource.set(postRestoreNanos);

tracer.notifyLambdaStart(new Object(), "lambda-request-123");

assertEquals(postRestoreNanos, tracer.getTimeWithNanoTicks(timeSource.getNanoTicks()));
} finally {
tracer.close();
}
}

@Test
void
notifyLambdaStart_whenResyncDefaultsToDisabled_thenTimestampStaysAnchoredToConstructionTime() {
ControllableTimeSource timeSource = new ControllableTimeSource();
timeSource.set(TimeUnit.SECONDS.toNanos(1000));
CoreTracer tracer = tracerBuilder().writer(new ListWriter()).timeSource(timeSource).build();
try {
long constructionTimeNanoTicks = timeSource.getNanoTicks();
timeSource.set(TimeUnit.SECONDS.toNanos(1000) + TimeUnit.HOURS.toNanos(2));

// No @WithConfig override here - this is an opt-in feature, off by default.
tracer.notifyLambdaStart(new Object(), "lambda-request-123");

assertEquals(
TimeUnit.SECONDS.toNanos(1000), tracer.getTimeWithNanoTicks(constructionTimeNanoTicks));
} finally {
tracer.close();
}
}

@Test
@WithConfig(key = TracerConfig.PRIORITY_SAMPLING, value = "false")
void verifyOverridingSampler() {
Expand Down
12 changes: 12 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_EXPERIMENTAL_FEATURES_ENABLED;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_HTTP_RESOURCE_REMOVE_TRAILING_SLASH;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_KEEP_LATENCY_THRESHOLD_MS;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_ENABLED;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_FLUSH_INTERVAL;
import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_INITIAL_FLUSH_INTERVAL;
Expand Down Expand Up @@ -691,6 +692,7 @@
import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING;
import static datadog.trace.api.config.TracerConfig.TRACE_INFERRED_PROXY_SERVICES_ENABLED;
import static datadog.trace.api.config.TracerConfig.TRACE_KEEP_LATENCY_THRESHOLD_MS;
import static datadog.trace.api.config.TracerConfig.TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED;
import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_ENABLED;
import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_FLUSH_INTERVAL;
import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_INITIAL_FLUSH_INTERVAL;
Expand Down Expand Up @@ -1317,6 +1319,8 @@ public static String getHostName() {

private final boolean secureRandom;

private final boolean lambdaSnapStartClockResyncEnabled;

private final boolean trace128bitTraceIdGenerationEnabled;
private final boolean logs128bitTraceIdEnabled;

Expand Down Expand Up @@ -1521,6 +1525,10 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins
} else {
secureRandom = configProvider.getBoolean(SECURE_RANDOM, DEFAULT_SECURE_RANDOM);
}
lambdaSnapStartClockResyncEnabled =
configProvider.getBoolean(
TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED,
DEFAULT_TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED);
cassandraKeyspaceStatementExtractionEnabled =
configProvider.getBoolean(
CASSANDRA_KEYSPACE_STATEMENT_EXTRACTION_ENABLED,
Expand Down Expand Up @@ -4977,6 +4985,10 @@ public boolean isAwsServerless() {
return awsServerless;
}

public boolean isLambdaSnapStartClockResyncEnabled() {
return lambdaSnapStartClockResyncEnabled;
}

public boolean isDataStreamsEnabled() {
return dataStreamsEnabled;
}
Expand Down
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -5097,6 +5097,14 @@
"aliases": []
}
],
"DD_TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "false",
"aliases": []
}
],
"DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH": [
{
"version": "A",
Expand Down