Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fbe14337 authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Add additional logging to PersistentConnectionManager" into main

parents d70ec3fd 50711c1f
Loading
Loading
Loading
Loading
+236 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.util.service

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.dump.dumpManager
import com.android.systemui.testKosmos
import com.android.systemui.util.service.ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED
import com.android.systemui.util.time.fakeSystemClock
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class PersistentConnectionManagerTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    private val fakeClock = kosmos.fakeSystemClock
    private val fakeExecutor = kosmos.fakeExecutor

    private class Proxy {
        // Fake proxy class
    }

    private val connection: ObservableServiceConnection<Proxy> = mock()
    private val observer: Observer = mock()

    private val underTest: PersistentConnectionManager<Proxy> by lazy {
        PersistentConnectionManager(
            /* clock = */ fakeClock,
            /* bgExecutor = */ fakeExecutor,
            /* dumpManager = */ kosmos.dumpManager,
            /* dumpsysName = */ DUMPSYS_NAME,
            /* serviceConnection = */ connection,
            /* maxReconnectAttempts = */ MAX_RETRIES,
            /* baseReconnectDelayMs = */ RETRY_DELAY_MS,
            /* minConnectionDurationMs = */ CONNECTION_MIN_DURATION_MS,
            /* observer = */ observer
        )
    }

    /** Validates initial connection. */
    @Test
    fun testConnect() {
        underTest.start()
        captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>())
    }

    /** Ensures reconnection on disconnect. */
    @Test
    fun testExponentialRetryOnDisconnect() {
        underTest.start()

        // IF service is connected...
        val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
        verify(connection, times(1)).bind()
        verify(connection).addCallback(captor.capture())
        val callback = captor.lastValue
        callback.onConnected(connection, mock<Proxy>())

        // ...AND service becomes disconnected within CONNECTION_MIN_DURATION_MS
        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)

        // THEN verify we retry to bind after the retry delay. (RETRY #1)
        verify(connection, times(1)).bind()
        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
        verify(connection, times(2)).bind()

        // IF service becomes disconnected for a second time after first retry...
        callback.onConnected(connection, mock<Proxy>())
        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)

        // THEN verify we retry after a longer delay of 2 * RETRY_DELAY_MS (RETRY #2)
        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
        verify(connection, times(2)).bind()
        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
        verify(connection, times(3)).bind()

        // IF service becomes disconnected for a third time after the second retry...
        callback.onConnected(connection, mock<Proxy>())
        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)

        // THEN verify we retry after a longer delay of 4 * RETRY_DELAY_MS (RETRY #3)
        fakeClock.advanceTime(3 * RETRY_DELAY_MS.toLong())
        verify(connection, times(3)).bind()
        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
        verify(connection, times(4)).bind()
    }

    @Test
    fun testDoesNotRetryAfterMaxRetries() {
        underTest.start()

        val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
        verify(connection).addCallback(captor.capture())
        val callback = captor.lastValue

        // IF we retry MAX_TRIES times...
        for (attemptCount in 0 until MAX_RETRIES + 1) {
            verify(connection, times(attemptCount + 1)).bind()
            callback.onConnected(connection, mock<Proxy>())
            callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
            fakeClock.advanceTime(Math.scalb(RETRY_DELAY_MS.toDouble(), attemptCount).toLong())
        }

        // THEN we should not retry again after the last attempt.
        fakeExecutor.advanceClockToLast()
        verify(connection, times(MAX_RETRIES + 1)).bind()
    }

    @Test
    fun testEnsureNoRetryIfServiceNeverConnectsAfterRetry() {
        underTest.start()

        with(captureCallbackAndVerifyBind(connection)) {
            // IF service initially connects and then disconnects...
            onConnected(connection, mock<Proxy>())
            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
            fakeExecutor.advanceClockToLast()
            fakeExecutor.runAllReady()

            // ...AND we retry once.
            verify(connection, times(1)).bind()

            // ...AND service disconnects after initial retry without ever connecting again.
            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
            fakeExecutor.advanceClockToLast()
            fakeExecutor.runAllReady()

            // THEN verify another retry is not triggered.
            verify(connection, times(1)).bind()
        }
    }

    @Test
    fun testEnsureNoRetryIfServiceNeverInitiallyConnects() {
        underTest.start()

        with(captureCallbackAndVerifyBind(connection)) {
            // IF service never connects and we just receive the disconnect signal...
            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
            fakeExecutor.advanceClockToLast()
            fakeExecutor.runAllReady()

            // THEN do not retry
            verify(connection, never()).bind()
        }
    }

    /** Ensures manual unbind does not reconnect. */
    @Test
    fun testStopDoesNotReconnect() {
        underTest.start()

        val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
        verify(connection).addCallback(connectionCallbackCaptor.capture())
        verify(connection).bind()
        clearInvocations(connection)

        underTest.stop()
        fakeExecutor.advanceClockToNext()
        fakeExecutor.runAllReady()
        verify(connection, never()).bind()
    }

    /** Ensures rebind on package change. */
    @Test
    fun testAttemptOnPackageChange() {
        underTest.start()

        verify(connection).bind()

        val callbackCaptor = argumentCaptor<Observer.Callback>()
        captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>())

        verify(observer).addCallback(callbackCaptor.capture())
        callbackCaptor.lastValue.onSourceChanged()
        verify(connection).bind()
    }

    @Test
    fun testAddConnectionCallback() {
        val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock()
        underTest.addConnectionCallback(connectionCallback)
        verify(connection).addCallback(connectionCallback)
    }

    @Test
    fun testRemoveConnectionCallback() {
        val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock()
        underTest.removeConnectionCallback(connectionCallback)
        verify(connection).removeCallback(connectionCallback)
    }

    /** Helper method to capture the [ObservableServiceConnection.Callback] */
    private fun captureCallbackAndVerifyBind(
        mConnection: ObservableServiceConnection<Proxy>,
    ): ObservableServiceConnection.Callback<Proxy> {

        val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
        verify(mConnection).addCallback(connectionCallbackCaptor.capture())
        verify(mConnection).bind()
        clearInvocations(mConnection)

        return connectionCallbackCaptor.lastValue
    }

    companion object {
        private const val MAX_RETRIES = 3
        private const val RETRY_DELAY_MS = 1000
        private const val CONNECTION_MIN_DURATION_MS = 5000
        private const val DUMPSYS_NAME = "dumpsys_name"
    }
}
+50 −34
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.util.Log;

import androidx.annotation.NonNull;

import com.android.app.tracing.TraceStateLogger;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
@@ -41,11 +42,11 @@ import javax.inject.Named;
/**
 * The {@link PersistentConnectionManager} is responsible for maintaining a connection to a
 * {@link ObservableServiceConnection}.
 *
 * @param <T> The transformed connection type handled by the service.
 */
public class PersistentConnectionManager<T> implements Dumpable {
    private static final String TAG = "PersistentConnManager";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final SystemClock mSystemClock;
    private final DelayableExecutor mBgExecutor;
@@ -55,6 +56,7 @@ public class PersistentConnectionManager<T> implements Dumpable {
    private final Observer mObserver;
    private final DumpManager mDumpManager;
    private final String mDumpsysName;
    private final TraceStateLogger mConnectionReasonLogger;

    private int mReconnectAttempts = 0;
    private Runnable mCurrentReconnectCancelable;
@@ -64,16 +66,18 @@ public class PersistentConnectionManager<T> implements Dumpable {
    private final Runnable mConnectRunnable = new Runnable() {
        @Override
        public void run() {
            mConnectionReasonLogger.log("ConnectionReasonRetry");
            mCurrentReconnectCancelable = null;
            mConnection.bind();
        }
    };

    private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt();
    private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt(
            "ConnectionReasonObserver");

    private final ObservableServiceConnection.Callback<T> mConnectionCallback =
            new ObservableServiceConnection.Callback<>() {
        private long mStartTime;
                private long mStartTime = -1;

                @Override
                public void onConnected(ObservableServiceConnection connection, Object proxy) {
@@ -87,8 +91,22 @@ public class PersistentConnectionManager<T> implements Dumpable {
                        return;
                    }

            if (mSystemClock.currentTimeMillis() - mStartTime > mMinConnectionDuration) {
                initiateConnectionAttempt();
                    if (mStartTime <= 0) {
                        Log.e(TAG, "onDisconnected called with invalid connection start time: "
                                + mStartTime);
                        return;
                    }

                    final float connectionDuration = mSystemClock.currentTimeMillis() - mStartTime;
                    // Reset the start time.
                    mStartTime = -1;

                    if (connectionDuration > mMinConnectionDuration) {
                        Log.i(TAG, "immediately reconnecting since service was connected for "
                                + connectionDuration
                                + "ms which is longer than the min duration of "
                                + mMinConnectionDuration + "ms");
                        initiateConnectionAttempt("ConnectionReasonMinDurationMet");
                    } else {
                        scheduleConnectionAttempt();
                    }
@@ -112,6 +130,7 @@ public class PersistentConnectionManager<T> implements Dumpable {
        mObserver = observer;
        mDumpManager = dumpManager;
        mDumpsysName = TAG + "#" + dumpsysName;
        mConnectionReasonLogger = new TraceStateLogger(mDumpsysName);

        mMaxReconnectAttempts = maxReconnectAttempts;
        mBaseReconnectDelayMs = baseReconnectDelayMs;
@@ -125,7 +144,7 @@ public class PersistentConnectionManager<T> implements Dumpable {
        mDumpManager.registerCriticalDumpable(mDumpsysName, this);
        mConnection.addCallback(mConnectionCallback);
        mObserver.addCallback(mObserverCallback);
        initiateConnectionAttempt();
        initiateConnectionAttempt("ConnectionReasonStart");
    }

    /**
@@ -140,6 +159,7 @@ public class PersistentConnectionManager<T> implements Dumpable {

    /**
     * Add a callback to the {@link ObservableServiceConnection}.
     *
     * @param callback The callback to add.
     */
    public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
@@ -148,6 +168,7 @@ public class PersistentConnectionManager<T> implements Dumpable {

    /**
     * Remove a callback from the {@link ObservableServiceConnection}.
     *
     * @param callback The callback to remove.
     */
    public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
@@ -163,10 +184,10 @@ public class PersistentConnectionManager<T> implements Dumpable {
        mConnection.dump(pw);
    }

    private void initiateConnectionAttempt() {
    private void initiateConnectionAttempt(String reason) {
        mConnectionReasonLogger.log(reason);
        // Reset attempts
        mReconnectAttempts = 0;

        // The first attempt is always a direct invocation rather than delayed.
        mConnection.bind();
    }
@@ -179,20 +200,15 @@ public class PersistentConnectionManager<T> implements Dumpable {
        }

        if (mReconnectAttempts >= mMaxReconnectAttempts) {
            if (DEBUG) {
            Log.d(TAG, "exceeded max connection attempts.");
            }
            return;
        }

        final long reconnectDelayMs =
                (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts);

        if (DEBUG) {
        Log.d(TAG,
                "scheduling connection attempt in " + reconnectDelayMs + "milliseconds");
        }

        mCurrentReconnectCancelable = mBgExecutor.executeDelayed(mConnectRunnable,
                reconnectDelayMs);

+0 −178
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.util.service;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class PersistentConnectionManagerTest extends SysuiTestCase {
    private static final int MAX_RETRIES = 5;
    private static final int RETRY_DELAY_MS = 1000;
    private static final int CONNECTION_MIN_DURATION_MS = 5000;
    private static final String DUMPSYS_NAME = "dumpsys_name";

    private FakeSystemClock mFakeClock = new FakeSystemClock();
    private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock);

    @Mock
    private ObservableServiceConnection<Proxy> mConnection;

    @Mock
    private ObservableServiceConnection.Callback<Proxy> mConnectionCallback;

    @Mock
    private Observer mObserver;

    @Mock
    private DumpManager mDumpManager;

    private static class Proxy {
    }

    private PersistentConnectionManager<Proxy> mConnectionManager;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        mConnectionManager = new PersistentConnectionManager<>(
                mFakeClock,
                mFakeExecutor,
                mDumpManager,
                DUMPSYS_NAME,
                mConnection,
                MAX_RETRIES,
                RETRY_DELAY_MS,
                CONNECTION_MIN_DURATION_MS,
                mObserver);
    }

    private ObservableServiceConnection.Callback<Proxy> captureCallbackAndSend(
            ObservableServiceConnection<Proxy> mConnection, Proxy proxy) {
        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);

        verify(mConnection).addCallback(connectionCallbackCaptor.capture());
        verify(mConnection).bind();
        Mockito.clearInvocations(mConnection);

        final ObservableServiceConnection.Callback callback = connectionCallbackCaptor.getValue();
        if (proxy != null) {
            callback.onConnected(mConnection, proxy);
        } else {
            callback.onDisconnected(mConnection, 0);
        }

        return callback;
    }

    /**
     * Validates initial connection.
     */
    @Test
    public void testConnect() {
        mConnectionManager.start();
        captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class));
    }

    /**
     * Ensures reconnection on disconnect.
     */
    @Test
    public void testRetryOnBindFailure() {
        mConnectionManager.start();
        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);

        verify(mConnection).addCallback(connectionCallbackCaptor.capture());

        // Verify attempts happen. Note that we account for the retries plus initial attempt, which
        // is not scheduled.
        for (int attemptCount = 0; attemptCount < MAX_RETRIES + 1; attemptCount++) {
            verify(mConnection).bind();
            Mockito.clearInvocations(mConnection);
            connectionCallbackCaptor.getValue().onDisconnected(mConnection, 0);
            mFakeExecutor.advanceClockToNext();
            mFakeExecutor.runAllReady();
        }
    }

    /**
     * Ensures manual unbind does not reconnect.
     */
    @Test
    public void testStopDoesNotReconnect() {
        mConnectionManager.start();
        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);

        verify(mConnection).addCallback(connectionCallbackCaptor.capture());
        verify(mConnection).bind();
        Mockito.clearInvocations(mConnection);
        mConnectionManager.stop();
        mFakeExecutor.advanceClockToNext();
        mFakeExecutor.runAllReady();
        verify(mConnection, never()).bind();
    }

    /**
     * Ensures rebind on package change.
     */
    @Test
    public void testAttemptOnPackageChange() {
        mConnectionManager.start();
        verify(mConnection).bind();
        ArgumentCaptor<Observer.Callback> callbackCaptor =
                ArgumentCaptor.forClass(Observer.Callback.class);
        captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class));

        verify(mObserver).addCallback(callbackCaptor.capture());

        callbackCaptor.getValue().onSourceChanged();
        verify(mConnection).bind();
    }

    @Test
    public void testAddConnectionCallback() {
        mConnectionManager.addConnectionCallback(mConnectionCallback);
        verify(mConnection).addCallback(mConnectionCallback);
    }

    @Test
    public void testRemoveConnectionCallback() {
        mConnectionManager.removeConnectionCallback(mConnectionCallback);
        verify(mConnection).removeCallback(mConnectionCallback);
    }
}