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

Commit 18223cdd authored by Pengquan Meng's avatar Pengquan Meng Committed by Brad Ebinger
Browse files

Add exponential backoff to ImsServiceController

This add exponential backoff with jitter applied to
ImsServiceController to replace the constant time retry mechanism.

Test: "JUnit tests"
Bug:34635764

Merged-In: I3069c0144ff76429315768fc8d1f6e232e3aaa3c
Change-Id: I3069c0144ff76429315768fc8d1f6e232e3aaa3c
parent 1ad8b167
Loading
Loading
Loading
Loading
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.internal.telephony;

import android.annotation.NonNull;
import android.os.Handler;
import android.os.Looper;

/** The implementation of exponential backoff with jitter applied. */
public class ExponentialBackoff {
    private int mRetryCounter;
    private long mStartDelayMs;
    private long mMaximumDelayMs;
    private long mCurrentDelayMs;
    private int mMultiplier;
    private Runnable mRunnable;
    private Handler mHandler;

    public ExponentialBackoff(
            long initialDelayMs,
            long maximumDelayMs,
            int multiplier,
            @NonNull Looper looper,
            @NonNull Runnable runnable) {
        this(initialDelayMs, maximumDelayMs, multiplier, new Handler(looper), runnable);
    }

    public ExponentialBackoff(
            long initialDelayMs,
            long maximumDelayMs,
            int multiplier,
            @NonNull Handler handler,
            @NonNull Runnable runnable) {
        mRetryCounter = 0;
        mStartDelayMs = initialDelayMs;
        mMaximumDelayMs = maximumDelayMs;
        mMultiplier = multiplier;
        mHandler = handler;
        mRunnable = runnable;
    }

    /** Starts the backoff, the runnable will be executed after {@link #mStartDelayMs}. */
    public void start() {
        mRetryCounter = 0;
        mCurrentDelayMs = mStartDelayMs;
        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mCurrentDelayMs);
    }

    /** Stops the backoff, all pending messages will be removed from the message queue. */
    public void stop() {
        mRetryCounter = 0;
        mHandler.removeCallbacks(mRunnable);
    }

    /** Should call when the retry action has failed and we want to retry after a longer delay. */
    public void notifyFailed() {
        mRetryCounter++;
        long temp = Math.min(
                mMaximumDelayMs, (long) (mStartDelayMs * Math.pow(mMultiplier, mRetryCounter)));
        mCurrentDelayMs = (long) (((1 + Math.random()) / 2) * temp);
        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mCurrentDelayMs);
    }

    /** Returns the delay for the most recently posted message. */
    public long getCurrentDelay() {
        return mCurrentDelayMs;
    }
}
+47 −26
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.ims.internal.IImsServiceController;
import com.android.ims.internal.IImsServiceFeatureListener;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.ExponentialBackoff;

import java.util.HashSet;
import java.util.Iterator;
@@ -76,6 +77,7 @@ public class ImsServiceController {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBackoff.stop();
            synchronized (mLock) {
                mIsBound = true;
                mIsBinding = false;
@@ -142,21 +144,28 @@ public class ImsServiceController {
    @VisibleForTesting
    public interface RebindRetry {
        /**
         * Return a long in ms indiciating how long the ImsServiceController should wait before
         * rebinding.
         * Returns a long in ms indicating how long the ImsServiceController should wait before
         * rebinding for the first time.
         */
        long getRetryTimeout();
        long getStartDelay();

        /**
         * Returns a long in ms indicating the maximum time the ImsServiceController should wait
         * before rebinding.
         */
        long getMaximumDelay();
    }

    private static final String LOG_TAG = "ImsServiceController";
    private static final int REBIND_RETRY_TIME = 5000;
    private static final int REBIND_START_DELAY_MS = 2 * 1000; // 2 seconds
    private static final int REBIND_MAXIMUM_DELAY_MS = 60 * 1000; // 1 minute
    private final Context mContext;
    private final ComponentName mComponentName;
    private final Object mLock = new Object();
    private final HandlerThread mHandlerThread = new HandlerThread("ImsServiceControllerHandler");
    private final IPackageManager mPackageManager;
    private ImsServiceControllerCallbacks mCallbacks;
    private Handler mHandler;
    private ExponentialBackoff mBackoff;

    private boolean mIsBound = false;
    private boolean mIsBinding = false;
@@ -213,17 +222,17 @@ public class ImsServiceController {
        }
    };

    private RebindRetry mRebindRetry = () -> REBIND_RETRY_TIME;

    @VisibleForTesting
    public void setRebindRetryTime(RebindRetry retry) {
        mRebindRetry = retry;
    private RebindRetry mRebindRetry = new RebindRetry() {
        @Override
        public long getStartDelay() {
            return REBIND_START_DELAY_MS;
        }

    @VisibleForTesting
    public Handler getHandler() {
        return mHandler;
        @Override
        public long getMaximumDelay() {
            return REBIND_MAXIMUM_DELAY_MS;
        }
    };

    public ImsServiceController(Context context, ComponentName componentName,
            ImsServiceControllerCallbacks callbacks) {
@@ -231,7 +240,12 @@ public class ImsServiceController {
        mComponentName = componentName;
        mCallbacks = callbacks;
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
        mBackoff = new ExponentialBackoff(
                mRebindRetry.getStartDelay(),
                mRebindRetry.getMaximumDelay(),
                2, /* multiplier */
                mHandlerThread.getLooper(),
                mRestartImsServiceRunnable);
        mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
    }

@@ -239,11 +253,16 @@ public class ImsServiceController {
    // Creating a new HandlerThread and background handler for each test causes a segfault, so for
    // testing, use a handler supplied by the testing system.
    public ImsServiceController(Context context, ComponentName componentName,
            ImsServiceControllerCallbacks callbacks, Handler testHandler) {
            ImsServiceControllerCallbacks callbacks, Handler handler, RebindRetry rebindRetry) {
        mContext = context;
        mComponentName = componentName;
        mCallbacks = callbacks;
        mHandler = testHandler;
        mBackoff = new ExponentialBackoff(
                rebindRetry.getStartDelay(),
                rebindRetry.getMaximumDelay(),
                2, /* multiplier */
                handler,
                mRestartImsServiceRunnable);
        mPackageManager = null;
    }

@@ -258,8 +277,6 @@ public class ImsServiceController {
     */
    public boolean bind(HashSet<Pair<Integer, Integer>> imsFeatureSet) {
        synchronized (mLock) {
            // Remove pending rebind retry
            mHandler.removeCallbacks(mRestartImsServiceRunnable);
            if (!mIsBound && !mIsBinding) {
                mIsBinding = true;
                mImsFeatures = imsFeatureSet;
@@ -273,8 +290,10 @@ public class ImsServiceController {
                    return mContext.bindService(imsServiceIntent, mImsServiceConnection,
                            serviceFlags);
                } catch (Exception e) {
                    mBackoff.notifyFailed();
                    Log.e(LOG_TAG, "Error binding (" + mComponentName + ") with exception: "
                            + e.getMessage());
                            + e.getMessage() + ", rebinding in " + mBackoff.getCurrentDelay()
                            + " ms");
                    return false;
                }
            } else {
@@ -289,8 +308,7 @@ public class ImsServiceController {
     */
    public void unbind() throws RemoteException {
        synchronized (mLock) {
            // Remove pending rebind retry
            mHandler.removeCallbacks(mRestartImsServiceRunnable);
            mBackoff.stop();
            if (mImsServiceConnection == null || mImsDeathRecipient == null) {
                return;
            }
@@ -343,6 +361,11 @@ public class ImsServiceController {
        return mImsServiceControllerBinder;
    }

    @VisibleForTesting
    public long getRebindDelay() {
        return mBackoff.getCurrentDelay();
    }

    public ComponentName getComponentName() {
        return mComponentName;
    }
@@ -364,9 +387,7 @@ public class ImsServiceController {

    // Only add a new rebind if there are no pending rebinds waiting.
    private void startDelayedRebindToService() {
        if (!mHandler.hasCallbacks(mRestartImsServiceRunnable)) {
            mHandler.postDelayed(mRestartImsServiceRunnable, mRebindRetry.getRetryTimeout());
        }
        mBackoff.start();
    }

    // Grant runtime permissions to ImsService. PackageManager ensures that the ImsService is
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.internal.telephony;

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.os.Handler;
import android.os.Looper;
import android.support.test.runner.AndroidJUnit4;

import com.android.internal.telephony.ims.ImsTestBase;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class ExponentialBackoffTest extends ImsTestBase {

    private static final int START_DELAY_MS = 10;
    private static final int MAXIMUM_DELAY_MS = 1000;
    private static final int MULTIPLIER = 2;

    private ExponentialBackoff mBackoffUnderTest;
    private Handler mHandler = spy(new Handler(Looper.getMainLooper()));
    private Runnable mRunnable = spy(new MyRunnable());

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            // do nothing
        }
    }

    @Before
    public void setUp() {
        mBackoffUnderTest = new ExponentialBackoff(
                START_DELAY_MS, MAXIMUM_DELAY_MS, MULTIPLIER, mHandler, mRunnable);
    }

    @After
    public void tearDown() {
        mBackoffUnderTest.stop();
    }

    @Test
    public void testStartBackoff() {
        mBackoffUnderTest.start();
        long delay = mBackoffUnderTest.getCurrentDelay();
        waitForHandlerActionDelayed(mHandler, delay, 2 * delay);

        // The runnable is executed after timeout event occurred.
        verify(mRunnable).run();
    }

    @Test
    public void testStopBackoff() {
        mBackoffUnderTest.start();
        reset(mHandler);

        mBackoffUnderTest.stop();
        verify(mHandler).removeCallbacks(mRunnable);
    }

    @Test
    public void testDelayIncreasedExponentially() {
        mBackoffUnderTest.start();
        // guarantee START_DELAY_MS * 2 ^ i <= MAXIMUM_DELAY_MS
        for (int i = 1; i < 5; i++) {
            mBackoffUnderTest.notifyFailed();
            long delay = mBackoffUnderTest.getCurrentDelay();
            long minDelay = (long) (START_DELAY_MS * Math.pow(MULTIPLIER, i - 1));
            long maxDelay = (long) (START_DELAY_MS * Math.pow(MULTIPLIER, i));
            assertTrue("delay = " + delay + " minDelay = " + minDelay, delay >= minDelay);
            assertTrue("delay = " + delay + " maxDelay = " + maxDelay, delay <= maxDelay);
        }
    }

    @Test
    public void testDelayShouldNotExceededTheMaximumLimit() {
        mBackoffUnderTest.start();
        // guarantee START_DELAY_MS * 2 ^ 30 > MAXIMUM_DELAY_MS
        for (int i = 1; i < 30; i++) {
            mBackoffUnderTest.notifyFailed();
        }
        long delay = mBackoffUnderTest.getCurrentDelay();
        assertTrue(
                "delay = " + delay + " maximumDelay = " + MAXIMUM_DELAY_MS,
                delay <= MAXIMUM_DELAY_MS);
    }
}
+22 −15
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;

import com.android.ims.internal.IImsServiceFeatureListener;
import com.android.internal.telephony.ims.ImsServiceController.RebindRetry;

import org.junit.After;
import org.junit.Before;
@@ -60,7 +61,17 @@ import java.util.HashSet;
@Ignore
public class ImsServiceControllerTest extends ImsTestBase {

    private static final int RETRY_TIMEOUT = 50; // ms
    private static final RebindRetry REBIND_RETRY = new RebindRetry() {
        @Override
        public long getStartDelay() {
            return 50;
        }

        @Override
        public long getMaximumDelay() {
            return 1000;
        }
    };

    @Spy TestImsServiceControllerAdapter mMockServiceControllerBinder;
    @Mock IBinder mMockBinder;
@@ -69,15 +80,15 @@ public class ImsServiceControllerTest extends ImsTestBase {
    @Mock Context mMockContext;
    private final ComponentName mTestComponentName = new ComponentName("TestPkg",
            "ImsServiceControllerTest");
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private ImsServiceController mTestImsServiceController;
    private final Handler mTestHandler = new Handler(Looper.getMainLooper());

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();
        mTestImsServiceController = new ImsServiceController(mMockContext, mTestComponentName,
                mMockCallbacks, mTestHandler);
                mMockCallbacks, mHandler, REBIND_RETRY);
        mTestImsServiceController.addImsServiceFeatureListener(mMockProxyCallbacks);
        when(mMockContext.bindService(any(), any(), anyInt())).thenReturn(true);
    }
@@ -86,7 +97,6 @@ public class ImsServiceControllerTest extends ImsTestBase {
    @After
    @Override
    public void tearDown() throws Exception {
        mTestHandler.removeCallbacksAndMessages(null);
        mTestImsServiceController = null;
        super.tearDown();
    }
@@ -354,13 +364,12 @@ public class ImsServiceControllerTest extends ImsTestBase {
        testFeatures.add(new Pair<>(1, 1));
        testFeatures.add(new Pair<>(1, 2));
        bindAndConnectService(testFeatures);
        mTestImsServiceController.setRebindRetryTime(() -> RETRY_TIMEOUT);

        getDeathRecipient().binderDied();

        waitForHandlerActionDelayed(mTestImsServiceController.getHandler(), RETRY_TIMEOUT,
                2 * RETRY_TIMEOUT);
        // The service should autobind after RETRY_TIMEOUT occurs
        long delay = mTestImsServiceController.getRebindDelay();
        waitForHandlerActionDelayed(mHandler, delay, 2 * delay);
        // The service should autobind after rebind event occurs
        verify(mMockContext, times(2)).bindService(any(), any(), anyInt());
    }

@@ -374,7 +383,6 @@ public class ImsServiceControllerTest extends ImsTestBase {
        testFeatures.add(new Pair<>(1, 1));
        testFeatures.add(new Pair<>(1, 2));
        bindAndConnectService(testFeatures);
        mTestImsServiceController.setRebindRetryTime(() -> RETRY_TIMEOUT);

        getDeathRecipient().binderDied();

@@ -392,13 +400,13 @@ public class ImsServiceControllerTest extends ImsTestBase {
        testFeatures.add(new Pair<>(1, 1));
        testFeatures.add(new Pair<>(1, 2));
        bindAndConnectService(testFeatures);
        mTestImsServiceController.setRebindRetryTime(() -> RETRY_TIMEOUT);

        getDeathRecipient().binderDied();
        mTestImsServiceController.unbind();

        waitForHandlerActionDelayed(mTestImsServiceController.getHandler(), RETRY_TIMEOUT,
                2 * RETRY_TIMEOUT);
        long delay = mTestImsServiceController.getRebindDelay();
        waitForHandlerActionDelayed(mHandler, delay, 2 * delay);

        // Unbind should stop the autobind from occurring.
        verify(mMockContext, times(1)).bindService(any(), any(), anyInt());
    }
@@ -414,12 +422,11 @@ public class ImsServiceControllerTest extends ImsTestBase {
        testFeatures.add(new Pair<>(1, 1));
        testFeatures.add(new Pair<>(1, 2));
        bindAndConnectService(testFeatures);
        mTestImsServiceController.setRebindRetryTime(() -> RETRY_TIMEOUT);
        getDeathRecipient().binderDied();
        mTestImsServiceController.bind(testFeatures);

        waitForHandlerActionDelayed(mTestImsServiceController.getHandler(), RETRY_TIMEOUT,
                2 * RETRY_TIMEOUT);
        long delay = mTestImsServiceController.getRebindDelay();
        waitForHandlerActionDelayed(mHandler, delay, 2 * delay);
        // Should only see two binds, not three from the auto rebind that occurs.
        verify(mMockContext, times(2)).bindService(any(), any(), anyInt());
    }