|
18 | 18 |
|
19 | 19 | package org.apache.storm.daemon.drpc; |
20 | 20 |
|
| 21 | +import java.util.ArrayList; |
21 | 22 | import java.util.Collections; |
22 | 23 | import java.util.HashMap; |
| 24 | +import java.util.HashSet; |
| 25 | +import java.util.List; |
23 | 26 | import java.util.Map; |
| 27 | +import java.util.Set; |
24 | 28 | import java.util.concurrent.ExecutionException; |
25 | 29 | import java.util.concurrent.ExecutorService; |
26 | 30 | import java.util.concurrent.Executors; |
@@ -140,6 +144,107 @@ public void testDequeueAfterTimeout() throws Exception { |
140 | 144 | } |
141 | 145 | } |
142 | 146 |
|
| 147 | + @Test |
| 148 | + public void testQueuesAreRemovedWhenEmpty() throws Exception { |
| 149 | + try (DRPC server = new DRPC(new StormMetricsRegistry(), null, 1000)) { |
| 150 | + //Fetching for a function nothing was ever submitted for must not leave state behind |
| 151 | + DRPCRequest nothing = server.fetchRequest("never-registered"); |
| 152 | + assertNotNull(nothing); |
| 153 | + assertEquals("", nothing.get_request_id()); |
| 154 | + assertEquals(0, server.getNumTrackedFunctions()); |
| 155 | + |
| 156 | + //A registered function is still served repeatedly, and is not left behind once idle |
| 157 | + for (int i = 0; i < 3; i++) { |
| 158 | + Future<String> found = exec.submit(() -> server.executeBlocking("testing", "test")); |
| 159 | + DRPCRequest request = getNextAvailableRequest(server, "testing"); |
| 160 | + assertNotNull(request); |
| 161 | + server.returnResult(request.get_request_id(), "tested"); |
| 162 | + assertEquals("tested", found.get(10, TimeUnit.MILLISECONDS)); |
| 163 | + } |
| 164 | + assertEquals(0, server.getNumTrackedFunctions()); |
| 165 | + |
| 166 | + //Nor is a function whose only request timed out. The timer thread fails the request |
| 167 | + // before it drops the queue, so the caller can return first; wait for the drop instead |
| 168 | + // of racing it, with a hard timeout so a real leak still fails the test. |
| 169 | + try { |
| 170 | + server.executeBlocking("timing-out", "test"); |
| 171 | + fail("Should have timed out...."); |
| 172 | + } catch (DRPCExecutionException e) { |
| 173 | + assertEquals(DRPCExceptionType.SERVER_TIMEOUT, e.get_type()); |
| 174 | + } |
| 175 | + Awaitility.await("DRPC queue for timing-out to be dropped") |
| 176 | + .atMost(5, TimeUnit.SECONDS) |
| 177 | + .pollInterval(1, TimeUnit.MILLISECONDS) |
| 178 | + .until(() -> server.getNumTrackedFunctions() == 0); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + @Test |
| 183 | + public void testConcurrentExecuteAndFetchLosesNoRequests() throws Exception { |
| 184 | + //A bounded pool of 16 threads is what keeps this cheap: executeBlocking() parks its caller, |
| 185 | + // so an unbounded pool would need one live thread per outstanding request. The request |
| 186 | + // count costs no threads at all, and is what gives the stress test its power. Measured |
| 187 | + // against a fetchRequest() whose poll/remove escapes the per-function compute lock, the |
| 188 | + // lost request was caught 1 run in 25 at 200 requests, 3 in 10 at 2000 and 9 in 10 at 5000, |
| 189 | + // while a correct server still serves all 5000 in about a second with every core busy. |
| 190 | + final int numRequests = 5000; |
| 191 | + final int numThreads = 16; |
| 192 | + final long deadlineMs = 30_000; |
| 193 | + //A timeout far beyond the test deadline, so the cleanup timer never reaps a live request. |
| 194 | + try (DRPC server = new DRPC(new StormMetricsRegistry(), null, 300_000)) { |
| 195 | + ExecutorService submitters = Executors.newFixedThreadPool(numThreads); |
| 196 | + try { |
| 197 | + List<Future<String>> futures = new ArrayList<>(numRequests); |
| 198 | + for (int i = 0; i < numRequests; i++) { |
| 199 | + final String args = "test-" + i; |
| 200 | + futures.add(submitters.submit(() -> server.executeBlocking("testing", args))); |
| 201 | + } |
| 202 | + |
| 203 | + Set<String> servedIds = new HashSet<>(); |
| 204 | + long deadline = Time.currentTimeMillis() + deadlineMs; |
| 205 | + int emptyFetches = 0; |
| 206 | + while (servedIds.size() < numRequests) { |
| 207 | + if (Time.currentTimeMillis() > deadline) { |
| 208 | + fail("Only served " + servedIds.size() + " of " + numRequests |
| 209 | + + " requests within " + deadlineMs + "ms, a request was lost"); |
| 210 | + } |
| 211 | + DRPCRequest req = server.fetchRequest("testing"); |
| 212 | + assertNotNull(req); |
| 213 | + String id = req.get_request_id(); |
| 214 | + if (id.isEmpty()) { |
| 215 | + //Nothing to serve right now. Spin at first, so fetches keep interleaving |
| 216 | + // tightly with the submitting threads, and only back off if this goes on |
| 217 | + // for a long time (a regression, which the deadline above then fails). |
| 218 | + if (++emptyFetches > 10_000) { |
| 219 | + TimeUnit.MILLISECONDS.sleep(1); |
| 220 | + } else { |
| 221 | + Thread.onSpinWait(); |
| 222 | + } |
| 223 | + continue; |
| 224 | + } |
| 225 | + emptyFetches = 0; |
| 226 | + assertTrue(servedIds.add(id), "Request " + id + " was fetched more than once"); |
| 227 | + server.returnResult(id, "tested-" + id); |
| 228 | + } |
| 229 | + |
| 230 | + Set<String> results = new HashSet<>(); |
| 231 | + for (Future<String> f : futures) { |
| 232 | + long left = deadline - Time.currentTimeMillis(); |
| 233 | + assertTrue(left > 0, "Ran out of time waiting for the blocked callers"); |
| 234 | + assertTrue(results.add(f.get(left, TimeUnit.MILLISECONDS)), "Duplicate result returned"); |
| 235 | + } |
| 236 | + assertEquals(numRequests, results.size()); |
| 237 | + for (String id : servedIds) { |
| 238 | + assertTrue(results.contains("tested-" + id), "No caller got the result for " + id); |
| 239 | + } |
| 240 | + //Nothing is waiting any more, so no per-function queue may be left behind |
| 241 | + assertEquals(0, server.getNumTrackedFunctions()); |
| 242 | + } finally { |
| 243 | + submitters.shutdownNow(); |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + |
143 | 248 | @Test |
144 | 249 | public void testDeny() { |
145 | 250 | try (DRPC server = new DRPC(new StormMetricsRegistry(), new DenyAuthorizer(), 100)) { |
|
0 commit comments