Fix auto-download retry backoff

The new value never got stored in the database. Also, it only got
increased by certain types of errors - all other errors could be retried
indefinitely. Also added a unit test.
This commit is contained in:
ByteHamster 2021-10-31 22:14:41 +01:00
parent 345aad4148
commit 524e5c95fc
14 changed files with 84 additions and 77 deletions

View File

@ -146,7 +146,7 @@ public class PlaybackServiceTaskManagerTest {
FeedItem item = DBReader.getFeedItem(testItem.getId());
item.getMedia().setDownloaded(true);
item.getMedia().setFile_url("file://123");
item.setAutoDownload(false);
item.disableAutoDownload();
DBWriter.setFeedMedia(item.getMedia()).get();
DBWriter.setFeedItem(item).get();

View File

@ -34,7 +34,7 @@ public class CancelDownloadActionButton extends ItemActionButton {
FeedMedia media = item.getMedia();
DownloadRequester.getInstance().cancelDownload(context, media);
if (UserPreferences.isEnableAutodownload()) {
item.setAutoDownload(false);
item.disableAutoDownload();
DBWriter.setFeedItem(item);
}
}

View File

@ -113,7 +113,7 @@ public class DownloadLogFragment extends ListFragment {
if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId());
FeedItem feedItem = media.getItem();
feedItem.setAutoDownload(false);
feedItem.disableAutoDownload();
DBWriter.setFeedItem(feedItem);
}
} else if (item instanceof DownloadStatus) {

View File

@ -178,7 +178,7 @@ public class LocalFeedUpdater {
private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) {
FeedItem item = new FeedItem(0, file.getName(), UUID.randomUUID().toString(),
file.getName(), new Date(file.lastModified()), FeedItem.UNPLAYED, feed);
item.setAutoDownload(false);
item.disableAutoDownload();
long size = file.length();
FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(),

View File

@ -321,18 +321,8 @@ public class DownloadService extends Service {
if (item == null) {
return;
}
boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST;
boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE;
boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE;
if (! (unknownHost || unsupportedType || wrongSize)) {
try {
DBWriter.saveFeedItemAutoDownloadFailed(item).get();
} catch (ExecutionException | InterruptedException e) {
Log.d(TAG, "Ignoring exception while setting item download status");
e.printStackTrace();
}
}
item.increaseFailedAutoDownloadAttempts(System.currentTimeMillis());
DBWriter.setFeedItem(item);
// to make lists reload the failed item, we fake an item update
EventBus.getDefault().post(FeedItemEvent.updated(item));
}

View File

@ -83,7 +83,7 @@ public class MediaDownloadedHandler implements Runnable {
// we've received the media, we don't want to autodownload it again
if (item != null) {
item.setAutoDownload(false);
item.disableAutoDownload();
// setFeedItem() signals (via EventBus) that the item has been updated,
// so we do it after the enclosing media has been updated above,
// to ensure subscribers will get the updated FeedMedia as well

View File

@ -65,7 +65,7 @@ public class AutomaticDownloadAlgorithm {
Iterator<FeedItem> it = candidates.iterator();
while (it.hasNext()) {
FeedItem item = it.next();
if (!item.isAutoDownloadable() || FeedItemUtil.isPlaying(item.getMedia())
if (!item.isAutoDownloadable(System.currentTimeMillis()) || FeedItemUtil.isPlaying(item.getMedia())
|| item.getFeed().isLocalFeed()) {
it.remove();
}

View File

@ -73,7 +73,7 @@ class DBUpgrader {
}
if (oldVersion <= 9) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD
+ " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ " INTEGER DEFAULT 1");
}
if (oldVersion <= 10) {
@ -121,10 +121,10 @@ class DBUpgrader {
}
if (oldVersion <= 14) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " INTEGER");
+ " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER");
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " = "
+ "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD
+ " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " = "
+ "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ " FROM " + PodDBAdapter.TABLE_NAME_FEEDS
+ " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID
+ " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")");

View File

@ -950,25 +950,6 @@ public class DBWriter {
});
}
public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) {
return dbExec.submit(() -> {
int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1;
long autoDownload;
if (!feedItem.getAutoDownload() || failedAttempts >= 10) {
autoDownload = 0; // giving up, disable auto download
feedItem.setAutoDownload(false);
} else {
long now = System.currentTimeMillis();
autoDownload = (now / 10) * 10 + failedAttempts;
}
final PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
adapter.setFeedItemAutoDownload(feedItem, autoDownload);
adapter.close();
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
});
}
/**
* Set filter of the feed
*

View File

@ -97,7 +97,8 @@ public class PodDBAdapter {
public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
public static final String KEY_CHAPTER_TYPE = "type";
public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
public static final String KEY_AUTO_DOWNLOAD = "auto_download";
public static final String KEY_AUTO_DOWNLOAD_ATTEMPTS = "auto_download";
public static final String KEY_AUTO_DOWNLOAD_ENABLED = "auto_download"; // Both tables use the same key
public static final String KEY_KEEP_UPDATED = "keep_updated";
public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action";
public static final String KEY_FEED_VOLUME_ADAPTION = "feed_volume_adaption";
@ -141,7 +142,7 @@ public class PodDBAdapter {
+ KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
+ " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_TYPE + " TEXT,"
+ KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1,"
+ KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD_ENABLED + " INTEGER DEFAULT 1,"
+ KEY_USERNAME + " TEXT,"
+ KEY_PASSWORD + " TEXT,"
+ KEY_INCLUDE_FILTER + " TEXT DEFAULT '',"
@ -169,7 +170,7 @@ public class PodDBAdapter {
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ KEY_IMAGE_URL + " TEXT,"
+ KEY_AUTO_DOWNLOAD + " INTEGER)";
+ KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)";
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
@ -246,7 +247,7 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL,
TABLE_NAME_FEEDS + "." + KEY_TYPE,
TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED,
TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED,
TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
@ -295,7 +296,7 @@ public class PodDBAdapter {
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD;
+ TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS;
private static final String KEYS_FEED_MEDIA =
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
@ -445,7 +446,7 @@ public class PodDBAdapter {
throw new IllegalArgumentException("Feed ID of preference must not be null");
}
ContentValues values = new ContentValues();
values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload());
values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.getAutoDownload());
values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated());
values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().ordinal());
values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger());
@ -649,7 +650,7 @@ public class PodDBAdapter {
}
values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters());
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
values.put(KEY_AUTO_DOWNLOAD, item.getAutoDownload());
values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
values.put(KEY_IMAGE_URL, item.getImageUrl());
if (item.getId() == 0) {
@ -765,13 +766,6 @@ public class PodDBAdapter {
return status.getId();
}
public void setFeedItemAutoDownload(FeedItem feedItem, long autoDownload) {
ContentValues values = new ContentValues();
values.put(KEY_AUTO_DOWNLOAD, autoDownload);
db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
new String[]{String.valueOf(feedItem.getId())});
}
public void setFavorites(List<FeedItem> favorites) {
ContentValues values = new ContentValues();
try {

View File

@ -25,7 +25,7 @@ public abstract class FeedItemCursorMapper {
int indexHasChapters = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_CHAPTERS);
int indexRead = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_READ);
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD);
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
long id = cursor.getInt(indexId);

View File

@ -21,7 +21,7 @@ public abstract class FeedPreferencesCursorMapper {
@NonNull
public static FeedPreferences convert(@NonNull Cursor cursor) {
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD);
int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED);
int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED);
int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
int indexVolumeAdaption = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION);

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.core.feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import org.junit.Before;
import org.junit.Test;
@ -10,11 +11,13 @@ import java.util.Date;
import static de.danoeh.antennapod.core.feed.FeedItemMother.anyFeedItemWithImage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class FeedItemTest {
private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
private static final String TEXT_SHORT = "Lorem ipsum";
private static final long ONE_HOUR = 1000L * 3600L;
private FeedItem original;
private FeedItem changedFeedItem;
@ -136,4 +139,36 @@ public class FeedItemTest {
item.setDescriptionIfLonger(contentEncoded);
assertEquals(TEXT_LONG, item.getDescription());
}
}
@Test
public void testAutoDownloadBackoff() {
FeedItem item = new FeedItem();
item.setMedia(new FeedMedia(item, "https://example.com/file.mp3", 0, "audio/mpeg"));
long now = ONE_HOUR; // In reality, this is System.currentTimeMillis()
assertTrue(item.isAutoDownloadable(now));
item.increaseFailedAutoDownloadAttempts(now);
assertFalse(item.isAutoDownloadable(now));
now += ONE_HOUR;
assertTrue(item.isAutoDownloadable(now));
item.increaseFailedAutoDownloadAttempts(now);
assertFalse(item.isAutoDownloadable(now));
now += ONE_HOUR;
assertFalse(item.isAutoDownloadable(now)); // Should backoff, so more than 1 hour needed
now += ONE_HOUR;
assertTrue(item.isAutoDownloadable(now)); // Now it's enough
item.increaseFailedAutoDownloadAttempts(now);
item.increaseFailedAutoDownloadAttempts(now);
item.increaseFailedAutoDownloadAttempts(now);
now += 1000L * ONE_HOUR;
assertFalse(item.isAutoDownloadable(now)); // Should have given up
item.increaseFailedAutoDownloadAttempts(now);
now += 1000L * ONE_HOUR;
assertFalse(item.isAutoDownloadable(now)); // Still given up
}
}

View File

@ -64,12 +64,6 @@ public class FeedItem extends FeedComponent implements Serializable {
private transient List<Chapter> chapters;
private String imageUrl;
/*
* 0: auto download disabled
* 1: auto download enabled (default)
* > 1: auto download enabled, (approx.) timestamp of the last failed attempt
* where last digit denotes the number of failed attempts
*/
private long autoDownload = 1;
/**
@ -361,15 +355,18 @@ public class FeedItem extends FeedComponent implements Serializable {
return hasChapters;
}
public void setAutoDownload(boolean autoDownload) {
this.autoDownload = autoDownload ? 1 : 0;
public void disableAutoDownload() {
this.autoDownload = 0;
}
public boolean getAutoDownload() {
return this.autoDownload > 0;
public long getAutoDownloadAttemptsAndTime() {
return autoDownload;
}
public int getFailedAutoDownloadAttempts() {
// 0: auto download disabled
// 1: auto download enabled (default)
// > 1: auto download enabled, timestamp of last failed attempt, last digit denotes number of failed attempts
if (autoDownload <= 1) {
return 0;
}
@ -380,23 +377,33 @@ public class FeedItem extends FeedComponent implements Serializable {
return failedAttempts;
}
public boolean isDownloaded() {
return media != null && media.isDownloaded();
public void increaseFailedAutoDownloadAttempts(long now) {
if (autoDownload == 0) {
return; // Don't re-enable
}
int failedAttempts = getFailedAutoDownloadAttempts() + 1;
if (failedAttempts >= 5) {
disableAutoDownload(); // giving up
} else {
autoDownload = (now / 10) * 10 + failedAttempts;
}
}
public boolean isAutoDownloadable() {
public boolean isAutoDownloadable(long now) {
if (media == null || media.isDownloaded() || autoDownload == 0) {
return false;
}
if (autoDownload == 1) {
return true;
return true; // Never failed
}
int failedAttempts = getFailedAutoDownloadAttempts();
double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days
int millisecondsInHour = 3600000;
long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour);
long grace = TimeUnit.MINUTES.toMillis(5);
return System.currentTimeMillis() > (autoDownload + waitingTime - grace);
long waitingTime = TimeUnit.HOURS.toMillis((long) Math.pow(2, failedAttempts - 1));
long lastAttempt = (autoDownload / 10) * 10;
return now >= (lastAttempt + waitingTime);
}
public boolean isDownloaded() {
return media != null && media.isDownloaded();
}
/**