{"record":{"id":"28557ae9be7b11d4","repo":"elastic/elasticsearch","slug":"prefetchtime-prefetchtime-must-be-greater-tha","errorCode":null,"errorMessage":"prefetchTime [${prefetchTime}] must be greater than or equal to staleTime [${staleTime}]","messagePattern":"prefetchTime \\[(.+?)\\] must be greater than or equal to staleTime \\[(.+?)\\]","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"libs/workload-identity-aws/src/main/java/org/elasticsearch/workload/identity/aws/AsyncWebIdentityCredentialsProvider.java","lineNumber":103,"sourceCode":"\n    private AsyncWebIdentityCredentialsProvider(Builder builder) {\n        this.roleArn = Objects.requireNonNull(builder.roleArn, \"roleArn must not be null\");\n        this.roleSessionName = Objects.requireNonNull(builder.roleSessionName, \"roleSessionName must not be null\");\n        this.tokenSupplier = Objects.requireNonNull(builder.tokenSupplier, \"tokenSupplier must not be null\");\n        this.stsAsyncClient = Objects.requireNonNull(builder.stsAsyncClient, \"stsAsyncClient must not be null\");\n        this.prefetchTime = builder.prefetchTime != null ? builder.prefetchTime : DEFAULT_PREFETCH_TIME;\n        this.staleTime = builder.staleTime != null ? builder.staleTime : DEFAULT_STALE_TIME;\n        this.clock = builder.clock != null ? builder.clock : Clock.systemUTC();\n        if (staleTime.isNegative() || staleTime.isZero()) {\n            throw new IllegalArgumentException(\"staleTime must be a positive duration but was [\" + staleTime + \"]\");\n        }\n        if (prefetchTime.isNegative() || prefetchTime.isZero()) {\n            throw new IllegalArgumentException(\"prefetchTime must be a positive duration but was [\" + prefetchTime + \"]\");\n        }\n        // prefetchTime must start no later than staleTime, otherwise prefetchAt would fall after staleAt and the\n        // background-refresh window in resolveIdentity() would be unreachable.\n        if (prefetchTime.compareTo(staleTime) < 0) {\n            throw new IllegalArgumentException(\n                \"prefetchTime [\" + prefetchTime + \"] must be greater than or equal to staleTime [\" + staleTime + \"]\"\n            );\n        }\n    }\n\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    @Override\n    public CompletableFuture<AwsCredentialsIdentity> resolveIdentity(ResolveIdentityRequest request) {\n        Cached current = cache.get();\n        Instant now = clock.instant();\n        if (current == null || now.isAfter(current.staleAt())) {\n            // Nothing usable cached: wait on a refresh, but asynchronously, so the caller's thread is not parked.\n            return refresh().thenApply(Cached::credentials);\n        }\n        if (now.isAfter(current.prefetchAt())) {","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/libs/workload-identity-aws/src/main/java/org/elasticsearch/workload/identity/aws/AsyncWebIdentityCredentialsProvider.java#L85-L121","documentation":"A relational guard: prefetchTime must be >= staleTime. The comment in source explains that if prefetch starts later than stale, the prefetchAt instant would fall after staleAt and the background-refresh window in resolveIdentity() would be unreachable - so the provider would always hit the synchronous fallback. The check fires only after both individual positivity checks pass.","triggerScenarios":"Building the provider with prefetchTime strictly less than staleTime (both positive). E.g. prefetchTime = 10s, staleTime = 60s.","commonSituations":"Treating prefetchTime as \"lead time before expiry\" and staleTime as \"absolute window\" and inverting them; copying values from a doc that defined the terms differently; auto-derived values where prefetch shrinks below stale.","solutions":["Set prefetchTime >= staleTime (e.g. prefetch = stale, or prefetch larger)","If unsure, leave both null and accept the documented defaults, which already satisfy the invariant","Re-read the doc comment: prefetchTime is when background refresh kicks in, staleTime is when the cached value is considered unusable - prefetch must not be later than stale"],"exampleFix":"// before\n.prefetchTime(Duration.ofSeconds(10)).staleTime(Duration.ofSeconds(60)).build()\n// after\n.prefetchTime(Duration.ofSeconds(60)).staleTime(Duration.ofSeconds(60)).build()","handlingStrategy":"validation","validationCode":"if (prefetchTime != null && staleTime != null\n    && prefetchTime.compareTo(staleTime) < 0) {\n    throw new IllegalArgumentException(\"prefetchTime must be >= staleTime\");\n}\nbuilder.prefetchTime(prefetchTime).staleTime(staleTime);","typeGuard":"static boolean prefetchNotBeforeStale(Duration prefetch, Duration stale) {\n    return prefetch == null || stale == null || prefetch.compareTo(stale) >= 0;\n}","tryCatchPattern":"try { builder.prefetchTime(p).staleTime(s).build(); }\ncatch (IllegalArgumentException e) { /* align the two values */ }","preventionTips":["Validate the prefetch>=stale invariant in one place","Leave both null to use vetted defaults","Document the relationship in the config schema"],"tags":["aws","workload-identity","credentials","config","startup"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:31:55.035Z"}