Skip to content

Commit

Permalink
Sync: handle and cooperate on interruptions
Browse files Browse the repository at this point in the history
  • Loading branch information
UweTrottmann committed Feb 29, 2024
1 parent c00368b commit b750808
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2023 Uwe Trottmann
// SPDX-License-Identifier: Apache-2.0
// Copyright 2019-2024 Uwe Trottmann

package com.battlelancer.seriesguide.shows.tools

Expand Down Expand Up @@ -421,7 +421,11 @@ class AddUpdateShowTools @Inject constructor(

/**
* Updates a show. Adds new, updates changed and removes orphaned episodes.
*
* This runs coroutines blocking the current thread (see [updateWatchProviderMappings]).
* If it is interrupted, [InterruptedException] is thrown.
*/
@Throws(InterruptedException::class)
fun updateShow(showId: Long): UpdateResult {
val helper = SgRoomDatabase.getInstance(context).sgShow2Helper()
val show = helper.getShow(showId)
Expand Down Expand Up @@ -523,6 +527,7 @@ class AddUpdateShowTools @Inject constructor(
/**
* Download and store watch provider mappings if a streaming search region is configured.
*/
@Throws(InterruptedException::class)
private fun updateWatchProviderMappings(showId: Long, showTmdbId: Int) {
val region = StreamingSearch.getCurrentRegionOrNull(context) ?: return
runBlocking {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2023 Uwe Trottmann
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022-2024 Uwe Trottmann

package com.battlelancer.seriesguide.shows.tools

Expand Down Expand Up @@ -49,6 +49,7 @@ class ShowSync(
* On network errors retries a few times to update a show before failing.
*/
@SuppressLint("TimberExceptionLogging")
@Throws(InterruptedException::class)
fun sync(
context: Context,
currentTime: Long,
Expand All @@ -70,12 +71,15 @@ class ShowSync(
return UpdateResult.INCOMPLETE
}

if (Thread.interrupted()) throw InterruptedException()

// This can fail due to
// - network error (not connected, unknown host, time out) => abort and try again
// - API error (parsing error, other error) => abort, report and try again later
// - show does no longer exist => ignore and continue
// - database error => abort, report and try again later
// Note: reporting is done where the exception occurs.
// If this thread is interrupted throws InterruptedException
result = showTools.updateShow(showId)

if (result is ApiErrorRetry) {
Expand All @@ -89,22 +93,14 @@ class ShowSync(
return UpdateResult.INCOMPLETE
} else {
// Back off, then try again.
try {
// Wait for 2^n seconds + random milliseconds,
// with n starting at 0 (so 1 s + random ms)
val n = networkErrors - 1
Thread.sleep(
(2.0.pow(n)).toLong() * DateUtils.SECOND_IN_MILLIS
+ Random.nextInt(0, 1000)
)
} catch (e: InterruptedException) {
// This can happen if the system has decided to interrupt the sync
// thread (see AbstractThreadedSyncAdapter class documentation),
// just try again later.
Timber.v("Wait for retry interrupted by system, trying again later.")
progress.setImportantErrorIfNone("Interrupted by system, trying again later.")
return UpdateResult.INCOMPLETE
}
// Wait for 2^n seconds + random milliseconds,
// with n starting at 0 (so 1 s + random ms)
val n = networkErrors - 1
// If this thread is interrupted throws InterruptedException
Thread.sleep(
(2.0.pow(n)).toLong() * DateUtils.SECOND_IN_MILLIS
+ Random.nextInt(0, 1000)
)
}
} else if (networkErrors > 0) {
// Reduce counter on each successful update.
Expand All @@ -127,6 +123,7 @@ class ShowSync(
"Show '%s' removed from TMDB (id %s), maybe search for a replacement and remove it."
)
}

is ApiErrorRetry -> throw IllegalStateException("Should retry and not handle result.")
is ApiErrorStop -> {
// API error, do not continue and try again later.
Expand All @@ -140,6 +137,7 @@ class ShowSync(
)
return UpdateResult.INCOMPLETE
}

DatabaseError -> {
// Database error, do not continue and try again later.
setImportantMessageIfNone(
Expand Down Expand Up @@ -185,10 +183,12 @@ class ShowSync(
}
listOf(showId)
}

SyncType.FULL -> {
// get all show IDs for a full update
SgRoomDatabase.getInstance(context).sgShow2Helper().getShowIdsLong()
}

SyncType.DELTA -> getShowsToDeltaUpdate(context, currentTime)
else -> throw IllegalArgumentException("Sync type $syncType is not supported.")
}
Expand Down
174 changes: 104 additions & 70 deletions app/src/main/java/com/battlelancer/seriesguide/sync/SgSyncAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.battlelancer.seriesguide.SgApp
import com.battlelancer.seriesguide.backend.HexagonTools
import com.battlelancer.seriesguide.backend.settings.HexagonSettings
import com.battlelancer.seriesguide.jobs.NetworkJobProcessor
import com.battlelancer.seriesguide.lists.ListsTools2.migrateTvdbShowListItemsToTmdbIds
import com.battlelancer.seriesguide.lists.ListsTools2
import com.battlelancer.seriesguide.movies.tools.MovieTools
import com.battlelancer.seriesguide.notifications.NotificationService
import com.battlelancer.seriesguide.provider.SeriesGuideDatabase
Expand Down Expand Up @@ -102,94 +102,128 @@ class SgSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, tru

// from here on we need more sophisticated abort handling, so keep track of errors
val progress = SyncProgress()
try {
sync(showSync, currentTime, progress)
} catch (e: InterruptedException) {
// This can happen if the system has decided to interrupt the sync
// thread (see AbstractThreadedSyncAdapter class documentation),
// just try again later.
Timber.d("Sync interrupted by system, trying again later.")
progress.recordError()
progress.setImportantErrorIfNone("Interrupted by system, trying again later.")
}
progress.publishFinished()
}

@Throws(InterruptedException::class)
private fun sync(showSync: ShowSync, currentTime: Long, progress: SyncProgress) {
progress.publish(SyncProgress.Step.TMDB)

// Get latest TMDb configuration.
val tmdbSync = TmdbSync(context, tmdbConfigService.get(), movieTools.get())
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
tmdbSync.updateConfigurationAndWatchProviders(progress)

if (Thread.interrupted()) throw InterruptedException()

// Update show data.
// If failed for at least one show, do not proceed with other sync steps to avoid
// syncing with outdated show data.
// Note: it is still NOT guaranteed show data is up-to-date before syncing because a show
// does not get updated if it was recently (see ShowSync selecting which shows to update).
var resultCode = showSync.sync(context, currentTime, progress)
Timber.d("Syncing: TMDB shows...DONE")
if (resultCode == null || resultCode == UpdateResult.INCOMPLETE) {
progress.recordError()
progress.publishFinished()
if (showSync.isSyncMultiple) {
updateTimeAndFailedCounter(prefs, resultCode)
}
return // Try again later.
}

// do some more things if this is not a quick update
if (showSync.isSyncMultiple) {
// update data of to be released movies
if (!tmdbSync.updateMovies(progress)) {
// syncing with outdated show data. However, renew the search table and trigger the
// notification service if at least one show was updated.
// Note: it is still NOT guaranteed show data is up-to-date before syncing with Cloud/Trakt
// because a show does not get updated if it was recently (see ShowSync selecting which
// shows to update).
var hasAddedShows = false
try {
var resultCode = showSync.sync(context, currentTime, progress)
Timber.d("Syncing: TMDB shows...DONE")
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (resultCode == null || resultCode == UpdateResult.INCOMPLETE) {
progress.recordError()
if (showSync.isSyncMultiple) {
updateTimeAndFailedCounter(prefs, resultCode)
}
return // Try again later.
}
Timber.d("Syncing: TMDB...DONE")

// sync with hexagon
var hasAddedShows = false
val isHexagonEnabled = HexagonSettings.isEnabled(context)
if (isHexagonEnabled) {
val resultHexagonSync = HexagonSync(
context,
hexagonTools.get(), movieTools.get(), progress
).sync()
hasAddedShows = resultHexagonSync.hasAddedShows
// don't overwrite failure
if (resultCode == UpdateResult.SUCCESS) {
resultCode = if (resultHexagonSync.success) {
UpdateResult.SUCCESS
} else {
UpdateResult.INCOMPLETE

// do some more things if this is not a quick update
if (showSync.isSyncMultiple) {
if (Thread.interrupted()) throw InterruptedException()

// update data of to be released movies
if (!tmdbSync.updateMovies(progress)) {
progress.recordError()
}
Timber.d("Syncing: TMDB...DONE")

if (Thread.interrupted()) throw InterruptedException()

// sync with hexagon
val isHexagonEnabled = HexagonSettings.isEnabled(context)
if (isHexagonEnabled) {
val resultHexagonSync = HexagonSync(
context,
hexagonTools.get(), movieTools.get(), progress
).sync()
hasAddedShows = resultHexagonSync.hasAddedShows
// don't overwrite failure
if (resultCode == UpdateResult.SUCCESS) {
resultCode = if (resultHexagonSync.success) {
UpdateResult.SUCCESS
} else {
UpdateResult.INCOMPLETE
}
}
Timber.d("Syncing: Hexagon...DONE")
} else {
Timber.d("Syncing: Hexagon...SKIP")
}
Timber.d("Syncing: Hexagon...DONE")
} else {
Timber.d("Syncing: Hexagon...SKIP")
}

// Migrate legacy list items
// Note: might send to Hexagon, so make sure to sync lists with Hexagon before
migrateTvdbShowListItemsToTmdbIds(context)

// sync with trakt (only ratings if hexagon is enabled)
if (TraktCredentials.get(context).hasCredentials()) {
val resultTraktSync = TraktSync(
context, movieTools.get(),
traktSync.get(), progress
).sync(currentTime, isHexagonEnabled)
// don't overwrite failure
if (resultCode == UpdateResult.SUCCESS) {
resultCode = resultTraktSync
if (Thread.interrupted()) throw InterruptedException()

// Migrate legacy list items
// Note: might send to Hexagon, so make sure to sync lists with Hexagon before
ListsTools2.migrateTvdbShowListItemsToTmdbIds(context)

if (Thread.interrupted()) throw InterruptedException()

// sync with trakt (only ratings if hexagon is enabled)
if (TraktCredentials.get(context).hasCredentials()) {
val resultTraktSync = TraktSync(
context, movieTools.get(),
traktSync.get(), progress
).sync(currentTime, isHexagonEnabled)
// don't overwrite failure
if (resultCode == UpdateResult.SUCCESS) {
resultCode = resultTraktSync
}
Timber.d("Syncing: trakt...DONE")
} else {
Timber.d("Syncing: trakt...SKIP")
}
Timber.d("Syncing: trakt...DONE")
} else {
Timber.d("Syncing: trakt...SKIP")
}

// renew search table if shows were updated and it will not be renewed by add task
if (showSync.hasUpdatedShows() && !hasAddedShows) {
SeriesGuideDatabase.rebuildFtsTable(context)
}
if (Thread.interrupted()) throw InterruptedException()

// update next episodes for all shows
TaskManager.getInstance().tryNextEpisodeUpdateTask(context)
// update next episodes for all shows
TaskManager.getInstance().tryNextEpisodeUpdateTask(context)

updateTimeAndFailedCounter(prefs, resultCode)
}
updateTimeAndFailedCounter(prefs, resultCode)
}

// There could have been new episodes added after an update
NotificationService.trigger(context)
Timber.i("Syncing: %s", resultCode.toString())
} finally {
// Finish some things even if interrupted

Timber.i("Syncing: %s", resultCode.toString())
progress.publishFinished()
// Renew search table if shows were updated and it will not be renewed by add task,
// but as this is a little costly only do it when doing the less frequent multiple
// shows sync.
if (showSync.isSyncMultiple && showSync.hasUpdatedShows() && !hasAddedShows) {
SeriesGuideDatabase.rebuildFtsTable(context)
}
// There could have been new episodes added after an update
if (showSync.hasUpdatedShows()) {
NotificationService.trigger(context)
}
}
}

private fun updateTimeAndFailedCounter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public enum Step {
public static class SyncEvent {
@Nullable private final Step step;
@NonNull private final List<Step> stepsWithError;
@Nullable private String importantErrorOrNull;
@Nullable private final String importantErrorOrNull;

SyncEvent(
@Nullable Step step,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TmdbSync internal constructor(
private val movieTools: MovieTools
) {

@Throws(InterruptedException::class)
fun updateConfigurationAndWatchProviders(progress: SyncProgress) {
if (TmdbSettings.isConfigurationUpToDate(context)) {
return
Expand All @@ -39,6 +40,7 @@ class TmdbSync internal constructor(
// Show watch providers
StreamingSearch.getCurrentRegionOrNull(context)?.also {
// Note: only updating for shows to keep local watch provider filter up-to-date
// If this thread is interrupted throws InterruptedException
val providersUpdated = runBlocking(SgApp.SINGLE) {
StreamingSearch
.updateWatchProviders(context, SgWatchProvider.Type.SHOWS, it)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2023 Uwe Trottmann
// SPDX-License-Identifier: Apache-2.0
// Copyright 2021-2024 Uwe Trottmann

package com.battlelancer.seriesguide.shows.tools;

Expand Down Expand Up @@ -42,7 +42,7 @@ public void closeDb() {
}

@Test
public void test_singleNoId() {
public void test_singleNoId() throws InterruptedException {
SyncOptions.SyncType syncType = SyncOptions.SyncType.SINGLE;

ShowSync showSync = new ShowSync(syncType, 0);
Expand All @@ -52,7 +52,7 @@ public void test_singleNoId() {
}

@Test
public void test_fullNoShows() {
public void test_fullNoShows() throws InterruptedException {
SyncOptions.SyncType syncType = SyncOptions.SyncType.FULL;

ShowSync showSync = new ShowSync(syncType, 0);
Expand All @@ -62,7 +62,7 @@ public void test_fullNoShows() {
}

@Test
public void test_deltaNoShows() {
public void test_deltaNoShows() throws InterruptedException {
SyncOptions.SyncType syncType = SyncOptions.SyncType.DELTA;

ShowSync showSync = new ShowSync(syncType, 0);
Expand All @@ -72,7 +72,7 @@ public void test_deltaNoShows() {
}

@Nullable
private SgSyncAdapter.UpdateResult sync(ShowSync showSync) {
private SgSyncAdapter.UpdateResult sync(ShowSync showSync) throws InterruptedException {
return showSync.sync(ApplicationProvider.getApplicationContext(),
System.currentTimeMillis(), new SyncProgress());
}
Expand Down

0 comments on commit b750808

Please sign in to comment.