Fix a concurrency bug in the ranking reconsideration.

If we rely on mNotificationList to be sorted, then we cannot allow
records to change without a corresponding call to sort.  Currently
RankingFuture may modify records in a separate thread, while the sort
doesn't happen until later.  This creates a window for race conditions.

Instead, RankingFuture should record operations to be performed on the
record that will replayed later, in a transaction along with a sort.

We can't simply overwrite the old record completely because another
future may be concurrently modifying a different aspect of the record.
Two futures that attempt to modify the same aspect will be serialized
and the second will overwrite eventually the first.

Change-Id: I9223cabdc60f72d8e37e6d8119bea1e0127185c0
(cherry picked from commit 77d3e0d0297caca5358879d36e8ba77710eb8e82)
This commit is contained in:
Chris Wren
2014-05-21 15:28:10 -04:00
parent 0e1dc0f5aa
commit 470c1accf5
5 changed files with 89 additions and 100 deletions

View File

@@ -20,7 +20,6 @@ import android.app.Notification;
import android.content.Context;
import android.util.Slog;
import com.android.internal.R;
import com.android.server.notification.NotificationManagerService.NotificationRecord;
/**
@@ -39,7 +38,7 @@ public class NotificationIntrusivenessExtractor implements NotificationSignalExt
if (DBG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + ".");
}
public RankingFuture process(NotificationRecord record) {
public RankingReconsideration process(NotificationRecord record) {
if (record == null || record.getNotification() == null) {
if (DBG) Slog.d(TAG, "skipping empty notification");
return null;
@@ -54,10 +53,15 @@ public class NotificationIntrusivenessExtractor implements NotificationSignalExt
record.setRecentlyIntusive(true);
}
return new RankingFuture(record, HANG_TIME_MS) {
return new RankingReconsideration(record.getKey(), HANG_TIME_MS) {
@Override
public void work() {
mRecord.setRecentlyIntusive(false);
// pass
}
@Override
public void applyChangesLocked(NotificationRecord record) {
record.setRecentlyIntusive(false);
}
};
}

View File

@@ -111,7 +111,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/** {@hide} */
@@ -453,8 +452,7 @@ public class NotificationManagerService extends SystemService {
public static final class NotificationRecord
{
public static final class NotificationRecord {
final StatusBarNotification sbn;
SingleNotificationStats stats;
boolean isCanceled;
@@ -556,13 +554,13 @@ public class NotificationManagerService extends SystemService {
return mContactAffinity;
}
public boolean isRecentlyIntrusive() {
return mRecentlyIntrusive;
}
public void setRecentlyIntusive(boolean recentlyIntrusive) {
mRecentlyIntrusive = recentlyIntrusive;
}
public boolean isRecentlyIntrusive() {
return mRecentlyIntrusive;
}
}
private static final class ToastRecord
@@ -1635,8 +1633,8 @@ public class NotificationManagerService extends SystemService {
if (!mSignalExtractors.isEmpty()) {
for (NotificationSignalExtractor extractor : mSignalExtractors) {
try {
RankingFuture future = extractor.process(r);
scheduleRankingReconsideration(future);
RankingReconsideration recon = extractor.process(r);
scheduleRankingReconsideration(recon);
} catch (Throwable t) {
Slog.w(TAG, "NotificationSignalExtractor failed.", t);
}
@@ -1982,37 +1980,38 @@ public class NotificationManagerService extends SystemService {
}
}
private void scheduleRankingReconsideration(RankingFuture future) {
if (future != null) {
Message m = Message.obtain(mRankingHandler, MESSAGE_RECONSIDER_RANKING, future);
long delay = future.getDelay(TimeUnit.MILLISECONDS);
private void scheduleRankingReconsideration(RankingReconsideration recon) {
if (recon != null) {
Message m = Message.obtain(mRankingHandler, MESSAGE_RECONSIDER_RANKING, recon);
long delay = recon.getDelay(TimeUnit.MILLISECONDS);
mRankingHandler.sendMessageDelayed(m, delay);
}
}
private void handleRankingReconsideration(Message message) {
if (!(message.obj instanceof RankingFuture)) return;
RankingFuture future = (RankingFuture) message.obj;
future.run();
try {
NotificationRecord record = future.get();
synchronized (mNotificationList) {
int before = mNotificationList.indexOf(record);
if (before != -1) {
Collections.sort(mNotificationList, mRankingComparator);
int after = mNotificationList.indexOf(record);
if (before != after) {
scheduleSendRankingUpdate();
}
}
if (!(message.obj instanceof RankingReconsideration)) return;
RankingReconsideration recon = (RankingReconsideration) message.obj;
recon.run();
boolean orderChanged;
synchronized (mNotificationList) {
final NotificationRecord record = mNotificationsByKey.get(recon.getKey());
if (record == null) {
return;
}
} catch (InterruptedException e) {
// we're running the future explicitly, so this should never happen
} catch (ExecutionException e) {
// we're running the future explicitly, so this should never happen
int before = findNotificationRecordIndexLocked(record);
recon.applyChangesLocked(record);
Collections.sort(mNotificationList, mRankingComparator);
int after = findNotificationRecordIndexLocked(record);
orderChanged = before != after;
}
if (orderChanged) {
scheduleSendRankingUpdate();
}
}
// lock on mNotificationList
private int findNotificationRecordIndexLocked(NotificationRecord target) {
return Collections.binarySearch(mNotificationList, target, mRankingComparator);
}
private void scheduleSendRankingUpdate() {

View File

@@ -18,6 +18,8 @@ package com.android.server.notification;
import android.content.Context;
import com.android.server.notification.NotificationManagerService.NotificationRecord;
/**
* Extracts signals that will be useful to the {@link NotificationComparator} and caches them
* on the {@link NotificationManagerService.NotificationRecord} object. These annotations will
@@ -32,10 +34,10 @@ public interface NotificationSignalExtractor {
* Called once per notification that is posted or updated.
*
* @return null if the work is done, or a future if there is more to do. The
* {@link RankingFuture} will be run on a worker thread, and if notifications are re-ordered
* by that execution, the {@link NotificationManagerService} may send order update
* events to the {@link android.service.notification.NotificationListenerService}s.
* {@link RankingReconsideration} will be run on a worker thread, and if notifications
* are re-ordered by that execution, the {@link NotificationManagerService} may send order
* update events to the {@link android.service.notification.NotificationListenerService}s.
*/
public RankingFuture process(NotificationManagerService.NotificationRecord notification);
public RankingReconsideration process(NotificationRecord notification);
}

View File

@@ -15,14 +15,16 @@
*/
package com.android.server.notification;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.android.server.notification.NotificationManagerService.NotificationRecord;
public abstract class RankingFuture
implements ScheduledFuture<NotificationManagerService.NotificationRecord> {
import java.util.concurrent.TimeUnit;
/**
* Represents future work required to extract signals from notifications for ranking.
*
* {@hide}
*/
public abstract class RankingReconsideration implements Runnable {
private static final long IMMEDIATE = 0l;
private static final int START = 0;
@@ -32,18 +34,22 @@ public abstract class RankingFuture
private int mState;
private long mDelay;
protected NotificationManagerService.NotificationRecord mRecord;
protected String mKey;
public RankingFuture(NotificationManagerService.NotificationRecord record) {
this(record, IMMEDIATE);
public RankingReconsideration(String key) {
this(key, IMMEDIATE);
}
public RankingFuture(NotificationManagerService.NotificationRecord record, long delay) {
public RankingReconsideration(String key, long delay) {
mDelay = delay;
mRecord = record;
mKey = key;
mState = START;
}
public String getKey() {
return mKey;
}
public void run() {
if (mState == START) {
mState = RUNNING;
@@ -57,18 +63,10 @@ public abstract class RankingFuture
}
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(mDelay, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed another) {
return Long.compare(getDelay(TimeUnit.MILLISECONDS),
another.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (mState == START) { // can't cancel if running or done
mState = CANCELLED;
@@ -77,42 +75,25 @@ public abstract class RankingFuture
return false;
}
@Override
public boolean isCancelled() {
return mState == CANCELLED;
}
@Override
public boolean isDone() {
return mState == DONE;
}
@Override
public NotificationManagerService.NotificationRecord get()
throws InterruptedException, ExecutionException {
while (!isDone()) {
synchronized (this) {
this.wait();
}
}
return mRecord;
}
@Override
public NotificationManagerService.NotificationRecord get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
long timeoutMillis = unit.convert(timeout, TimeUnit.MILLISECONDS);
long start = System.currentTimeMillis();
long now = System.currentTimeMillis();
while (!isDone() && (now - start) < timeoutMillis) {
try {
wait(timeoutMillis - (now - start));
} catch (InterruptedException e) {
now = System.currentTimeMillis();
}
}
return mRecord;
}
/**
* Analyse the notification. This will be called on a worker thread. To
* avoid concurrency issues, do not use held references to modify the
* {@link NotificationRecord}.
*/
public abstract void work();
/**
* Apply any computed changes to the notification record. This method will be
* called on the main service thread, synchronized on he mNotificationList.
* @param record The locked record to be updated.
*/
public abstract void applyChangesLocked(NotificationRecord record);
}

View File

@@ -59,7 +59,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
// maps raw person handle to resolved person object
private LruCache<String, LookupResult> mPeopleCache;
private RankingFuture validatePeople(NotificationRecord record) {
private RankingReconsideration validatePeople(final NotificationRecord record) {
float affinity = NONE;
Bundle extras = record.getNotification().extras;
if (extras == null) {
@@ -99,11 +99,11 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
}
if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey());
return new RankingFuture(record) {
return new RankingReconsideration(record.getKey()) {
float mContactAffinity = NONE;
@Override
public void work() {
if (INFO) Slog.i(TAG, "Executing: validation for: " + mRecord.sbn.getKey());
float affinity = NONE;
if (INFO) Slog.i(TAG, "Executing: validation for: " + record.getKey());
for (final String handle: pendingLookups) {
LookupResult lookupResult = null;
final Uri uri = Uri.parse(handle);
@@ -124,13 +124,16 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
synchronized (mPeopleCache) {
mPeopleCache.put(handle, lookupResult);
}
affinity = Math.max(affinity, lookupResult.getAffinity());
mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity());
}
}
float affinityBound = mRecord.getContactAffinity();
affinity = Math.max(affinity, affinityBound);
mRecord.setContactAffinity(affinity);
if (INFO) Slog.i(TAG, "final affinity: " + affinity);
}
@Override
public void applyChangesLocked(NotificationRecord operand) {
float affinityBound = operand.getContactAffinity();
operand.setContactAffinity(Math.max(mContactAffinity, affinityBound));
if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity());
}
};
}
@@ -229,7 +232,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
}
public RankingFuture process(NotificationManagerService.NotificationRecord record) {
public RankingReconsideration process(NotificationRecord record) {
if (!mEnabled) {
if (INFO) Slog.i(TAG, "disabled");
return null;