1+ package ly .count .java .demo ;
2+
3+ import com .sun .net .httpserver .HttpServer ;
4+ import java .io .File ;
5+ import java .io .OutputStream ;
6+ import java .lang .reflect .Field ;
7+ import java .net .InetSocketAddress ;
8+ import java .util .concurrent .atomic .AtomicInteger ;
9+ import ly .count .sdk .java .Config ;
10+ import ly .count .sdk .java .Countly ;
11+
12+ /**
13+ * Reproduces GitHub Issue #264:
14+ * "Non-JSON Server Response Causes Permanent Networking Deadlock"
15+ *
16+ * This app starts a local HTTP server that returns HTML (simulating a 502 error page),
17+ * initializes the Countly SDK against it, records events, and checks whether the SDK
18+ * gets permanently stuck.
19+ *
20+ * Run with: ./gradlew app-java:run
21+ * (after setting mainClassName = 'ly.count.java.demo.ReproduceIssue264' in app-java/build.gradle)
22+ */
23+ public class ReproduceIssue264 {
24+
25+ public static void main (String [] args ) throws Exception {
26+ AtomicInteger requestCount = new AtomicInteger (0 );
27+ AtomicInteger successCount = new AtomicInteger (0 );
28+
29+ // Start a local HTTP server that returns HTML for the first 3 requests,
30+ // then valid JSON for subsequent requests (simulating server recovery)
31+ HttpServer server = HttpServer .create (new InetSocketAddress (0 ), 0 );
32+ int port = server .getAddress ().getPort ();
33+
34+ server .createContext ("/" , exchange -> {
35+ int count = requestCount .incrementAndGet ();
36+ String body ;
37+ int code ;
38+
39+ if (count <= 3 ) {
40+ code = 502 ;
41+ body = "<html><body><h1>502 Bad Gateway</h1><p>The server is temporarily unavailable.</p></body></html>" ;
42+ System .out .println ("[Mock Server] Request #" + count + " -> returning HTML 502 (simulating outage)" );
43+ } else {
44+ code = 200 ;
45+ body = "{\" result\" :\" Success\" }" ;
46+ successCount .incrementAndGet ();
47+ System .out .println ("[Mock Server] Request #" + count + " -> returning JSON 200 (server recovered)" );
48+ }
49+
50+ exchange .sendResponseHeaders (code , body .length ());
51+ OutputStream os = exchange .getResponseBody ();
52+ os .write (body .getBytes ());
53+ os .close ();
54+ });
55+
56+ server .start ();
57+ System .out .println ("=== Issue #264 Reproduction ===" );
58+ System .out .println ("[Mock Server] Started on port " + port );
59+ System .out .println ();
60+
61+ // Setup SDK storage directory
62+ String [] sdkStorageRootPath = { System .getProperty ("user.home" ), "__COUNTLY" , "java_issue264" };
63+ File sdkStorageRootDirectory = new File (String .join (File .separator , sdkStorageRootPath ));
64+ if (!(sdkStorageRootDirectory .exists () && sdkStorageRootDirectory .isDirectory ())) {
65+ sdkStorageRootDirectory .mkdirs ();
66+ }
67+
68+ // Initialize SDK pointing to our mock server
69+ Config config = new Config ("http://localhost:" + port , "TEST_APP_KEY" , sdkStorageRootDirectory )
70+ .setLoggingLevel (Config .LoggingLevel .WARN )
71+ .setDeviceIdStrategy (Config .DeviceIdStrategy .UUID )
72+ .enableFeatures (Config .Feature .Events , Config .Feature .Sessions )
73+ .setEventQueueSizeToSend (1 );
74+
75+ Countly .instance ().init (config );
76+ System .out .println ("[SDK] Initialized against mock server" );
77+
78+ // Start session (triggers first request -> will get HTML 502)
79+ Countly .session ().begin ();
80+ System .out .println ("[SDK] Session started" );
81+
82+ // Record an event (triggers another request -> will get HTML 502)
83+ Countly .instance ().events ().recordEvent ("test_event_during_outage" );
84+ System .out .println ("[SDK] Event recorded" );
85+
86+ // Wait for requests to be attempted
87+ System .out .println ();
88+ System .out .println ("[Test] Waiting 3 seconds for initial requests..." );
89+ Thread .sleep (3000 );
90+
91+ // Check if SDK is deadlocked via reflection (SDKCore.instance.networking is protected)
92+ boolean isSending = isNetworkingSending ();
93+
94+ System .out .println ();
95+ System .out .println ("============================================================" );
96+ if (isSending ) {
97+ System .out .println (" BUG REPRODUCED: isSending() = true (DEADLOCKED!)" );
98+ System .out .println (" The SDK is permanently stuck. No further requests" );
99+ System .out .println (" will ever be sent, even when the server recovers." );
100+ } else {
101+ System .out .println (" FIX CONFIRMED: isSending() = false (recovered)" );
102+ System .out .println (" The SDK handled the non-JSON response gracefully." );
103+ }
104+ System .out .println ("============================================================" );
105+ System .out .println ();
106+
107+ // Try to trigger recovery by calling check
108+ System .out .println ("[Test] Triggering networking check cycles (server now returns JSON)..." );
109+ triggerNetworkingChecks (5 );
110+
111+ int totalRequests = requestCount .get ();
112+ int successes = successCount .get ();
113+
114+ System .out .println ();
115+ System .out .println ("============================================================" );
116+ System .out .println (" Total requests received by server: " + totalRequests );
117+ System .out .println (" Successful (JSON 200) responses: " + successes );
118+ if (successes > 0 ) {
119+ System .out .println (" SDK successfully retried after server recovered!" );
120+ } else if (!isNetworkingSending ()) {
121+ System .out .println (" SDK recovered from error. Requests will retry on" );
122+ System .out .println (" the next timer tick (no deadlock)." );
123+ } else {
124+ System .out .println (" SDK is STILL deadlocked. Bug confirmed." );
125+ }
126+ System .out .println ("============================================================" );
127+
128+ // Cleanup
129+ Countly .instance ().stop ();
130+ server .stop (0 );
131+ System .out .println ();
132+ System .out .println ("[Done] Cleanup complete." );
133+ }
134+
135+ /**
136+ * Access SDKCore.instance.networking.isSending() via reflection
137+ * since these fields are protected/package-private.
138+ */
139+ private static boolean isNetworkingSending () throws Exception {
140+ Class <?> sdkCoreClass = Class .forName ("ly.count.sdk.java.internal.SDKCore" );
141+ Field instanceField = sdkCoreClass .getDeclaredField ("instance" );
142+ instanceField .setAccessible (true );
143+ Object sdkCore = instanceField .get (null );
144+
145+ Field networkingField = sdkCoreClass .getDeclaredField ("networking" );
146+ networkingField .setAccessible (true );
147+ Object networking = networkingField .get (sdkCore );
148+
149+ return (boolean ) networking .getClass ().getMethod ("isSending" ).invoke (networking );
150+ }
151+
152+ /**
153+ * Trigger SDKCore.instance.networking.check(config) via reflection.
154+ */
155+ private static void triggerNetworkingChecks (int count ) throws Exception {
156+ Class <?> sdkCoreClass = Class .forName ("ly.count.sdk.java.internal.SDKCore" );
157+ Field instanceField = sdkCoreClass .getDeclaredField ("instance" );
158+ instanceField .setAccessible (true );
159+ Object sdkCore = instanceField .get (null );
160+
161+ Field networkingField = sdkCoreClass .getDeclaredField ("networking" );
162+ networkingField .setAccessible (true );
163+ Object networking = networkingField .get (sdkCore );
164+
165+ Field configField = sdkCoreClass .getDeclaredField ("config" );
166+ configField .setAccessible (true );
167+ Object internalConfig = configField .get (sdkCore );
168+
169+ java .lang .reflect .Method checkMethod = networking .getClass ().getMethod ("check" ,
170+ Class .forName ("ly.count.sdk.java.internal.InternalConfig" ));
171+
172+ for (int i = 0 ; i < count ; i ++) {
173+ if (!isNetworkingSending ()) {
174+ checkMethod .invoke (networking , internalConfig );
175+ }
176+ Thread .sleep (1000 );
177+ }
178+ }
179+ }
0 commit comments