|
19 | 19 |
|
20 | 20 | package org.apache.pulsar.broker.service; |
21 | 21 |
|
| 22 | +import static org.mockito.ArgumentMatchers.any; |
| 23 | +import static org.mockito.ArgumentMatchers.anyLong; |
22 | 24 | import static org.mockito.Mockito.doAnswer; |
23 | 25 | import static org.mockito.Mockito.mock; |
24 | 26 | import static org.mockito.Mockito.when; |
25 | 27 | import static org.testng.Assert.assertEquals; |
| 28 | +import static org.testng.Assert.assertNotNull; |
| 29 | +import static org.testng.Assert.assertTrue; |
26 | 30 | import io.netty.channel.ChannelHandlerContext; |
27 | 31 | import io.netty.channel.DefaultEventLoop; |
28 | 32 | import io.netty.channel.EventLoop; |
29 | 33 | import io.netty.channel.EventLoopGroup; |
30 | 34 | import io.netty.util.concurrent.DefaultThreadFactory; |
| 35 | +import io.netty.util.concurrent.ScheduledFuture; |
31 | 36 | import java.util.HashMap; |
32 | 37 | import java.util.concurrent.CompletableFuture; |
33 | 38 | import java.util.concurrent.TimeUnit; |
| 39 | +import java.util.concurrent.atomic.AtomicInteger; |
34 | 40 | import java.util.concurrent.atomic.AtomicLong; |
| 41 | +import org.apache.pulsar.broker.qos.AsyncTokenBucket; |
35 | 42 | import org.apache.pulsar.common.policies.data.Policies; |
36 | 43 | import org.apache.pulsar.common.policies.data.PublishRate; |
37 | 44 | import org.testng.annotations.AfterMethod; |
@@ -149,4 +156,121 @@ public void testPublishRateLimiterImplUpdate() throws Exception { |
149 | 156 | }); |
150 | 157 | future.get(5, TimeUnit.SECONDS); |
151 | 158 | } |
| 159 | + |
| 160 | + /** |
| 161 | + * When the token bucket is deeply depleted, the first scheduled unthrottle uses a long delay. Disabling limits |
| 162 | + * must schedule an immediate unthrottle (delay 0) so producers are not stuck until that delay elapses. |
| 163 | + */ |
| 164 | + @Test |
| 165 | + public void shouldUnthrottleImmediatelyAfterDisablingLimitsDespiteLongPendingDelay() { |
| 166 | + AtomicLong manualClock = new AtomicLong(TimeUnit.SECONDS.toNanos(100)); |
| 167 | + AtomicInteger unthrottleCalls = new AtomicInteger(); |
| 168 | + |
| 169 | + PublishRateLimiterImpl limiter = new PublishRateLimiterImpl( |
| 170 | + manualClock::get, |
| 171 | + p -> { }, |
| 172 | + p -> unthrottleCalls.incrementAndGet()); |
| 173 | + |
| 174 | + EventLoop scheduler = mock(EventLoop.class); |
| 175 | + AtomicInteger longDelaySchedules = new AtomicInteger(); |
| 176 | + doAnswer(invocation -> { |
| 177 | + Runnable task = invocation.getArgument(0); |
| 178 | + long delay = invocation.getArgument(1); |
| 179 | + TimeUnit unit = invocation.getArgument(2); |
| 180 | + long delayNanos = unit.toNanos(delay); |
| 181 | + if (delayNanos == 0L) { |
| 182 | + task.run(); |
| 183 | + } else { |
| 184 | + longDelaySchedules.incrementAndGet(); |
| 185 | + } |
| 186 | + @SuppressWarnings("unchecked") |
| 187 | + ScheduledFuture<?> scheduled = mock(ScheduledFuture.class); |
| 188 | + return scheduled; |
| 189 | + }).when(scheduler).schedule(any(Runnable.class), anyLong(), any()); |
| 190 | + |
| 191 | + Producer p = mock(Producer.class); |
| 192 | + ServerCnx cnx = mock(ServerCnx.class); |
| 193 | + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); |
| 194 | + doAnswer(a -> ctx).when(cnx).ctx(); |
| 195 | + doAnswer(a -> cnx).when(p).getCnx(); |
| 196 | + when(p.getCnx()).thenReturn(cnx); |
| 197 | + doAnswer(a -> { |
| 198 | + ((Runnable) a.getArgument(0)).run(); |
| 199 | + return null; |
| 200 | + }).when(cnx).execute(any(Runnable.class)); |
| 201 | + |
| 202 | + BrokerService brokerService = mock(BrokerService.class); |
| 203 | + when(cnx.getBrokerService()).thenReturn(brokerService); |
| 204 | + EventLoopGroup eventLoopGroup = mock(EventLoopGroup.class); |
| 205 | + when(brokerService.executor()).thenReturn(eventLoopGroup); |
| 206 | + when(eventLoopGroup.next()).thenReturn(scheduler); |
| 207 | + |
| 208 | + limiter.update(new PublishRate(1, 0)); |
| 209 | + manualClock.addAndGet(TimeUnit.SECONDS.toNanos(1)); |
| 210 | + |
| 211 | + limiter.handlePublishThrottling(p, 100_000, 0L); |
| 212 | + assertEquals(unthrottleCalls.get(), 0); |
| 213 | + assertTrue(longDelaySchedules.get() >= 1, |
| 214 | + "Expected a long-delay unthrottle to be scheduled while the bucket is deeply depleted"); |
| 215 | + |
| 216 | + limiter.update(new PublishRate(0, 0)); |
| 217 | + assertEquals(unthrottleCalls.get(), 1); |
| 218 | + } |
| 219 | + |
| 220 | + /** |
| 221 | + * Relaxing only the byte limit still invalidates a previously scheduled long unthrottle delay; an immediate |
| 222 | + * unthrottle pass must run after buckets are rebuilt. |
| 223 | + */ |
| 224 | + @Test |
| 225 | + public void shouldUnthrottleImmediatelyAfterRaisingByteLimitDespiteLongPendingDelay() { |
| 226 | + AtomicLong manualClock = new AtomicLong(TimeUnit.SECONDS.toNanos(100)); |
| 227 | + AtomicInteger unthrottleCalls = new AtomicInteger(); |
| 228 | + |
| 229 | + PublishRateLimiterImpl limiter = new PublishRateLimiterImpl( |
| 230 | + manualClock::get, |
| 231 | + p -> { }, |
| 232 | + p -> unthrottleCalls.incrementAndGet()); |
| 233 | + |
| 234 | + EventLoop scheduler = mock(EventLoop.class); |
| 235 | + doAnswer(invocation -> { |
| 236 | + Runnable task = invocation.getArgument(0); |
| 237 | + long delay = invocation.getArgument(1); |
| 238 | + TimeUnit unit = invocation.getArgument(2); |
| 239 | + if (unit.toNanos(delay) == 0L) { |
| 240 | + task.run(); |
| 241 | + } |
| 242 | + @SuppressWarnings("unchecked") |
| 243 | + ScheduledFuture<?> scheduled = mock(ScheduledFuture.class); |
| 244 | + return scheduled; |
| 245 | + }).when(scheduler).schedule(any(Runnable.class), anyLong(), any()); |
| 246 | + |
| 247 | + Producer p = mock(Producer.class); |
| 248 | + ServerCnx cnx = mock(ServerCnx.class); |
| 249 | + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); |
| 250 | + doAnswer(a -> ctx).when(cnx).ctx(); |
| 251 | + doAnswer(a -> cnx).when(p).getCnx(); |
| 252 | + when(p.getCnx()).thenReturn(cnx); |
| 253 | + doAnswer(a -> { |
| 254 | + ((Runnable) a.getArgument(0)).run(); |
| 255 | + return null; |
| 256 | + }).when(cnx).execute(any(Runnable.class)); |
| 257 | + |
| 258 | + BrokerService brokerService = mock(BrokerService.class); |
| 259 | + when(cnx.getBrokerService()).thenReturn(brokerService); |
| 260 | + EventLoopGroup eventLoopGroup = mock(EventLoopGroup.class); |
| 261 | + when(brokerService.executor()).thenReturn(eventLoopGroup); |
| 262 | + when(eventLoopGroup.next()).thenReturn(scheduler); |
| 263 | + |
| 264 | + limiter.update(new PublishRate(0, 1)); |
| 265 | + manualClock.addAndGet(TimeUnit.SECONDS.toNanos(1)); |
| 266 | + |
| 267 | + limiter.handlePublishThrottling(p, 0, 100_000L); |
| 268 | + assertEquals(unthrottleCalls.get(), 0); |
| 269 | + |
| 270 | + limiter.update(new PublishRate(0, 1_000_000)); |
| 271 | + assertEquals(unthrottleCalls.get(), 1); |
| 272 | + |
| 273 | + AsyncTokenBucket byteBucket = limiter.getTokenBucketOnByte(); |
| 274 | + assertNotNull(byteBucket); |
| 275 | + } |
152 | 276 | } |
0 commit comments