From 1cc01e0b0b41c82ad6f6b6cdd04dd0dda8556b40 Mon Sep 17 00:00:00 2001 From: amirmohammad Date: Tue, 10 Feb 2026 00:09:47 +0330 Subject: [PATCH 1/2] Fix JDK test task dependencies by wiring to source set outputs (#4072) The testJdkN tasks copied classpath and testClassesDirs from the built-in test task, which broke the Gradle task dependency graph. Re-running a test task would not recompile main/test sources because no dependency on compilation tasks was established. Wire directly to sourceSets.test outputs instead, giving Gradle implicit task dependencies through file collection producers. This also eliminates the tasks.getByName("test") eager realization anti-pattern. --- retrofit/java-test/build.gradle | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/retrofit/java-test/build.gradle b/retrofit/java-test/build.gradle index 4129d512d4..bf28c56423 100644 --- a/retrofit/java-test/build.gradle +++ b/retrofit/java-test/build.gradle @@ -25,10 +25,9 @@ tasks.withType(JavaCompile).configureEach { description = "Runs the test suite on JDK $majorVersion" group = LifecycleBasePlugin.VERIFICATION_GROUP - // Copy inputs from normal Test task. - def testTask = tasks.getByName("test") - classpath = testTask.classpath - testClassesDirs = testTask.testClassesDirs + // Wire directly to the test source set outputs for proper task dependencies. + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath } tasks.named("check").configure { dependsOn(jdkTest) From ccb58fb4e484f4144820f5dc7cf917a181b112da Mon Sep 17 00:00:00 2001 From: amirmohammad Date: Sun, 15 Feb 2026 16:24:18 +0330 Subject: [PATCH 2/2] =?UTF-8?q?Changes=20to=20retrofit/src/main/java/retro?= =?UTF-8?q?fit2/OkHttpCall.java:=201.=20Imports:=20Added=20AtomicBoolean?= =?UTF-8?q?=20and=20AtomicReference;=20removed=20GuardedBy=202.=20Fields:?= =?UTF-8?q?=20Replaced=20rawCall=20+=20creationFailure=20+=20executed=20wi?= =?UTF-8?q?th=20AtomicBoolean=20executed=20and=20AtomicReference?= =?UTF-8?q?=20rawCallState=20(merged=20call/failure=20into=20one=20atomic?= =?UTF-8?q?=20ref)=20=20=203.=20getRawCall():=20Rewrote=20with=20CAS=20?= =?UTF-8?q?=E2=80=94=20reads=20state=20atomically,=20creates=20call=20on?= =?UTF-8?q?=20miss,=20uses=20compareAndSet=20to=20publish.=20Loser=20of=20?= =?UTF-8?q?a=20race=20uses=20the=20winner's=20result=20=20=204.=20throwSta?= =?UTF-8?q?teFailure():=20New=20static=20helper=20to=20rethrow=20stored=20?= =?UTF-8?q?failures=20=20=205.=20request()=20/=20timeout():=20Removed=20sy?= =?UTF-8?q?nchronized=20keyword=20(now=20lock-free=20via=20getRawCall())?= =?UTF-8?q?=20=20=206.=20execute():=20Uses=20executed.compareAndSet(false,?= =?UTF-8?q?=20true)=20instead=20of=20synchronized=20block=20=20=207.=20enq?= =?UTF-8?q?ueue():=20Same=20CAS=20for=20executed=20check;=20delegates=20to?= =?UTF-8?q?=20getRawCall()=20instead=20of=20inlining=20creation=20=20=208.?= =?UTF-8?q?=20isExecuted():=20Returns=20executed.get()=20instead=20of=20sy?= =?UTF-8?q?nchronized=20read=20=20=209.=20cancel()=20/=20isCanceled():=20R?= =?UTF-8?q?ead=20rawCallState.get()=20instead=20of=20synchronized=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/retrofit2/OkHttpCall.java | 111 ++++++++---------- 1 file changed, 50 insertions(+), 61 deletions(-) diff --git a/retrofit/src/main/java/retrofit2/OkHttpCall.java b/retrofit/src/main/java/retrofit2/OkHttpCall.java index ff67612263..0f20d02874 100644 --- a/retrofit/src/main/java/retrofit2/OkHttpCall.java +++ b/retrofit/src/main/java/retrofit2/OkHttpCall.java @@ -19,8 +19,9 @@ import java.io.IOException; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.ResponseBody; @@ -39,14 +40,13 @@ final class OkHttpCall implements Call { private volatile boolean canceled; - @GuardedBy("this") - private @Nullable okhttp3.Call rawCall; + private final AtomicBoolean executed = new AtomicBoolean(); - @GuardedBy("this") // Either a RuntimeException, non-fatal Error, or IOException. - private @Nullable Throwable creationFailure; - - @GuardedBy("this") - private boolean executed; + /** + * One of: null (not yet created), an {@link okhttp3.Call} (success), or a {@link Throwable} + * (creation failure). Once set to a non-null value, this reference never changes. + */ + private final AtomicReference rawCallState = new AtomicReference<>(); OkHttpCall( RequestFactory requestFactory, @@ -68,7 +68,7 @@ public OkHttpCall clone() { } @Override - public synchronized Request request() { + public Request request() { try { return getRawCall().request(); } catch (IOException e) { @@ -77,7 +77,7 @@ public synchronized Request request() { } @Override - public synchronized Timeout timeout() { + public Timeout timeout() { try { return getRawCall().timeout(); } catch (IOException e) { @@ -89,53 +89,49 @@ public synchronized Timeout timeout() { * Returns the raw call, initializing it if necessary. Throws if initializing the raw call throws, * or has thrown in previous attempts to create it. */ - @GuardedBy("this") private okhttp3.Call getRawCall() throws IOException { - okhttp3.Call call = rawCall; - if (call != null) return call; - - // Re-throw previous failures if this isn't the first attempt. - if (creationFailure != null) { - if (creationFailure instanceof IOException) { - throw (IOException) creationFailure; - } else if (creationFailure instanceof RuntimeException) { - throw (RuntimeException) creationFailure; - } else { - throw (Error) creationFailure; - } - } + Object state = rawCallState.get(); + if (state instanceof okhttp3.Call) return (okhttp3.Call) state; + if (state != null) throwStateFailure(state); - // Create and remember either the success or the failure. try { - return rawCall = createRawCall(); + okhttp3.Call call = createRawCall(); + if (rawCallState.compareAndSet(null, call)) return call; + // Another thread resolved state concurrently — use their result. + state = rawCallState.get(); + if (state instanceof okhttp3.Call) return (okhttp3.Call) state; + throwStateFailure(state); + throw new AssertionError(); // unreachable } catch (RuntimeException | Error | IOException e) { - throwIfFatal(e); // Do not assign a fatal error to creationFailure. - creationFailure = e; + throwIfFatal(e); // Do not assign a fatal error to rawCallState. + rawCallState.compareAndSet(null, e); throw e; } } + private static void throwStateFailure(Object state) throws IOException { + if (state instanceof IOException) throw (IOException) state; + if (state instanceof RuntimeException) throw (RuntimeException) state; + throw (Error) state; + } + @Override public void enqueue(final Callback callback) { Objects.requireNonNull(callback, "callback == null"); + if (!executed.compareAndSet(false, true)) { + throw new IllegalStateException("Already executed."); + } + okhttp3.Call call; Throwable failure; - - synchronized (this) { - if (executed) throw new IllegalStateException("Already executed."); - executed = true; - - call = rawCall; - failure = creationFailure; - if (call == null && failure == null) { - try { - call = rawCall = createRawCall(); - } catch (Throwable t) { - throwIfFatal(t); - failure = creationFailure = t; - } - } + try { + call = getRawCall(); + failure = null; + } catch (Throwable t) { + throwIfFatal(t); + call = null; + failure = t; } if (failure != null) { @@ -185,21 +181,18 @@ private void callFailure(Throwable e) { } @Override - public synchronized boolean isExecuted() { - return executed; + public boolean isExecuted() { + return executed.get(); } @Override public Response execute() throws IOException { - okhttp3.Call call; - - synchronized (this) { - if (executed) throw new IllegalStateException("Already executed."); - executed = true; - - call = getRawCall(); + if (!executed.compareAndSet(false, true)) { + throw new IllegalStateException("Already executed."); } + okhttp3.Call call = getRawCall(); + if (canceled) { call.cancel(); } @@ -257,12 +250,9 @@ Response parseResponse(okhttp3.Response rawResponse) throws IOException { public void cancel() { canceled = true; - okhttp3.Call call; - synchronized (this) { - call = rawCall; - } - if (call != null) { - call.cancel(); + Object state = rawCallState.get(); + if (state instanceof okhttp3.Call) { + ((okhttp3.Call) state).cancel(); } } @@ -271,9 +261,8 @@ public boolean isCanceled() { if (canceled) { return true; } - synchronized (this) { - return rawCall != null && rawCall.isCanceled(); - } + Object state = rawCallState.get(); + return state instanceof okhttp3.Call && ((okhttp3.Call) state).isCanceled(); } static final class NoContentResponseBody extends ResponseBody {