|
11 | 11 | import static org.mockito.Mockito.never; |
12 | 12 | import static org.mockito.Mockito.spy; |
13 | 13 | import static org.mockito.Mockito.timeout; |
| 14 | +import static org.mockito.Mockito.doReturn; |
14 | 15 | import static org.mockito.Mockito.times; |
15 | 16 | import static org.mockito.Mockito.verify; |
16 | 17 | import static org.mockito.Mockito.verifyNoInteractions; |
@@ -151,6 +152,62 @@ public void flushInsertsPoison() throws InterruptedException { |
151 | 152 | verify(messageQueue).put(FlushMessage.POISON); |
152 | 153 | } |
153 | 154 |
|
| 155 | + @Test |
| 156 | + public void rateLimitedDeferralPreservesOverflowMessage() throws InterruptedException { |
| 157 | + // Use a real queue with real messages. Two large messages trigger batchSizeLimitReached |
| 158 | + // on the second one. StopMessage ends the Looper (bypasses rate-limit on shutdown). |
| 159 | + LinkedBlockingQueue<Message> localQueue = new LinkedBlockingQueue<>(); |
| 160 | + |
| 161 | + // Create messages large enough that the second exceeds BATCH_MAX_SIZE (512000 bytes). |
| 162 | + // ~300KB each: first fits, second triggers batchSizeLimitReached. |
| 163 | + String largePayload = new String(new char[300000]).replace('\0', 'x'); |
| 164 | + Map<String, Object> largeProps = new java.util.HashMap<>(); |
| 165 | + largeProps.put("data", largePayload); |
| 166 | + |
| 167 | + TrackMessage firstMessage = |
| 168 | + TrackMessage.builder("first").userId("user").properties(largeProps).build(); |
| 169 | + TrackMessage overflowMessage = |
| 170 | + TrackMessage.builder("overflow").userId("user").properties(largeProps).build(); |
| 171 | + |
| 172 | + localQueue.put(firstMessage); |
| 173 | + localQueue.put(overflowMessage); |
| 174 | + localQueue.put(StopMessage.STOP); |
| 175 | + |
| 176 | + // Pass isShutDown=true to prevent the constructor from auto-starting a Looper |
| 177 | + // (which would race with our manually-created Looper and consume queue messages). |
| 178 | + AnalyticsClient client = |
| 179 | + new AnalyticsClient( |
| 180 | + localQueue, |
| 181 | + null, |
| 182 | + segmentService, |
| 183 | + 50, |
| 184 | + TimeUnit.HOURS.toMillis(1), |
| 185 | + 0, |
| 186 | + MAX_BATCH_SIZE, |
| 187 | + log, |
| 188 | + threadFactory, |
| 189 | + networkExecutor, |
| 190 | + Collections.singletonList(callback), |
| 191 | + new AtomicBoolean(true), |
| 192 | + writeKey, |
| 193 | + new Gson(), |
| 194 | + DEFAULT_MAX_TOTAL_BACKOFF_DURATION_MS, |
| 195 | + DEFAULT_MAX_RATE_LIMIT_DURATION_MS); |
| 196 | + |
| 197 | + // Set rate-limited state so the Looper defers batch submission |
| 198 | + client.setRateLimitState(60); |
| 199 | + |
| 200 | + AnalyticsClient.Looper looper = client.new Looper(); |
| 201 | + looper.run(); |
| 202 | + |
| 203 | + // After: msg1 added to messages, msg2 triggers batchSizeLimitReached, |
| 204 | + // rate-limited deferral offers msg2 back to queue, StopMessage bypasses |
| 205 | + // rate-limit and submits batch with msg1. msg2 remains in queue. |
| 206 | + assertThat(localQueue).contains(overflowMessage); |
| 207 | + // Batch with msg1 was submitted on StopMessage (shutdown always flushes) |
| 208 | + verify(networkExecutor).submit(any(Runnable.class)); |
| 209 | + } |
| 210 | + |
154 | 211 | /** Wait until the queue is drained. */ |
155 | 212 | static void wait(Queue<?> queue) { |
156 | 213 | // noinspection StatementWithEmptyBody |
|
0 commit comments