diff --git a/services/core/java/com/android/server/connectivity/DnsEventListenerService.java b/services/core/java/com/android/server/connectivity/DnsEventListenerService.java index ae13db0ba95cd..8d206ef90b94a 100644 --- a/services/core/java/com/android/server/connectivity/DnsEventListenerService.java +++ b/services/core/java/com/android/server/connectivity/DnsEventListenerService.java @@ -17,16 +17,17 @@ package com.android.server.connectivity; import android.content.Context; -import android.net.metrics.DnsEvent; -import android.net.metrics.IpConnectivityLog; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkRequest; +import android.net.metrics.DnsEvent; import android.net.metrics.IDnsEventListener; +import android.net.metrics.IpConnectivityLog; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import java.io.PrintWriter; @@ -46,6 +47,7 @@ public class DnsEventListenerService extends IDnsEventListener.Stub { private static final boolean DBG = true; private static final boolean VDBG = false; + // TODO: read this constant from system property private static final int MAX_LOOKUPS_PER_DNS_EVENT = 100; // Stores the results of a number of consecutive DNS lookups on the same network. @@ -97,14 +99,14 @@ public class DnsEventListenerService extends IDnsEventListener.Stub { // Only sorted for ease of debugging. Because we only typically have a handful of networks up // at any given time, performance is not a concern. @GuardedBy("this") - private SortedMap mEventBatches = new TreeMap<>(); + private final SortedMap mEventBatches = new TreeMap<>(); // We register a NetworkCallback to ensure that when a network disconnects, we flush the DNS // queries we've logged on that network. Because we do not do this periodically, we might lose // up to MAX_LOOKUPS_PER_DNS_EVENT lookup stats on each network when the system is shutting // down. We believe this to be sufficient for now. private final ConnectivityManager mCm; - private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); + private final IpConnectivityLog mMetricsLog; private final NetworkCallback mNetworkCallback = new NetworkCallback() { @Override public void onLost(Network network) { @@ -118,11 +120,15 @@ public class DnsEventListenerService extends IDnsEventListener.Stub { }; public DnsEventListenerService(Context context) { + this(context.getSystemService(ConnectivityManager.class), new IpConnectivityLog()); + } + + @VisibleForTesting + public DnsEventListenerService(ConnectivityManager cm, IpConnectivityLog log) { // We are started when boot is complete, so ConnectivityService should already be running. - final NetworkRequest request = new NetworkRequest.Builder() - .clearCapabilities() - .build(); - mCm = context.getSystemService(ConnectivityManager.class); + mCm = cm; + mMetricsLog = log; + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); mCm.registerNetworkCallback(request, mNetworkCallback); } diff --git a/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java new file mode 100644 index 0000000000000..033b2c96c8f5f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.net.ConnectivityManager.NetworkCallback; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.metrics.DnsEvent; +import android.net.metrics.IDnsEventListener; +import android.net.metrics.IpConnectivityLog; + +import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.IntStream; + +public class DnsEventListenerServiceTest extends TestCase { + + // TODO: read from DnsEventListenerService after this constant is read from system property + static final int BATCH_SIZE = 100; + static final int EVENT_TYPE = IDnsEventListener.EVENT_GETADDRINFO; + // TODO: read from IDnsEventListener + static final int RETURN_CODE = 1; + + static final byte[] EVENT_TYPES = new byte[BATCH_SIZE]; + static final byte[] RETURN_CODES = new byte[BATCH_SIZE]; + static final int[] LATENCIES = new int[BATCH_SIZE]; + static { + for (int i = 0; i < BATCH_SIZE; i++) { + EVENT_TYPES[i] = EVENT_TYPE; + RETURN_CODES[i] = RETURN_CODE; + LATENCIES[i] = i; + } + } + + DnsEventListenerService mDnsService; + + @Mock ConnectivityManager mCm; + @Mock IpConnectivityLog mLog; + ArgumentCaptor mCallbackCaptor; + ArgumentCaptor mEvCaptor; + + public void setUp() { + MockitoAnnotations.initMocks(this); + mCallbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class); + mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); + mDnsService = new DnsEventListenerService(mCm, mLog); + + verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture()); + } + + public void testOneBatch() throws Exception { + log(105, LATENCIES); + log(106, Arrays.copyOf(LATENCIES, BATCH_SIZE - 1)); // one lookup short of a batch event + + verifyLoggedEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES)); + + log(106, Arrays.copyOfRange(LATENCIES, BATCH_SIZE - 1, BATCH_SIZE)); + + mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor + verifyLoggedEvents( + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES)); + } + + public void testSeveralBatches() throws Exception { + log(105, LATENCIES); + log(106, LATENCIES); + log(105, LATENCIES); + log(107, LATENCIES); + + verifyLoggedEvents( + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES)); + } + + public void testBatchAndNetworkLost() throws Exception { + byte[] eventTypes = Arrays.copyOf(EVENT_TYPES, 20); + byte[] returnCodes = Arrays.copyOf(RETURN_CODES, 20); + int[] latencies = Arrays.copyOf(LATENCIES, 20); + + log(105, LATENCIES); + log(105, latencies); + mCallbackCaptor.getValue().onLost(new Network(105)); + log(105, LATENCIES); + + verifyLoggedEvents( + new DnsEvent(105, eventTypes, returnCodes, latencies), + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES)); + } + + public void testConcurrentBatchesAndDumps() throws Exception { + final long stop = System.currentTimeMillis() + 100; + final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null")); + new Thread() { + public void run() { + while (System.currentTimeMillis() < stop) { + mDnsService.dump(pw); + } + } + }.start(); + + logAsync(105, LATENCIES); + logAsync(106, LATENCIES); + logAsync(107, LATENCIES); + + verifyLoggedEvents(500, + new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES), + new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES)); + } + + public void testConcurrentBatchesAndNetworkLoss() throws Exception { + logAsync(105, LATENCIES); + Thread.sleep(10L); + // call onLost() asynchronously to logAsync's onDnsEvent() calls. + mCallbackCaptor.getValue().onLost(new Network(105)); + + // do not verify unpredictable batch + verify(mLog, timeout(500).times(1)).log(any()); + } + + void log(int netId, int[] latencies) { + for (int l : latencies) { + mDnsService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l); + } + } + + void logAsync(int netId, int[] latencies) { + new Thread() { + public void run() { + log(netId, latencies); + } + }.start(); + } + + void verifyLoggedEvents(DnsEvent... expected) { + verifyLoggedEvents(0, expected); + } + + void verifyLoggedEvents(int wait, DnsEvent... expectedEvents) { + verify(mLog, timeout(wait).times(expectedEvents.length)).log(mEvCaptor.capture()); + for (DnsEvent got : mEvCaptor.getAllValues()) { + OptionalInt index = IntStream.range(0, expectedEvents.length) + .filter(i -> eventsEqual(expectedEvents[i], got)) + .findFirst(); + // Don't match same expected event more than once. + index.ifPresent(i -> expectedEvents[i] = null); + assertTrue(index.isPresent()); + } + } + + /** equality function for DnsEvent to avoid overriding equals() and hashCode(). */ + static boolean eventsEqual(DnsEvent expected, DnsEvent got) { + return (expected == got) || ((expected != null) && (got != null) + && (expected.netId == got.netId) + && Arrays.equals(expected.eventTypes, got.eventTypes) + && Arrays.equals(expected.returnCodes, got.returnCodes) + && Arrays.equals(expected.latenciesMs, got.latenciesMs)); + } +}