Correctly handle DeadObjectException (bitfireAT/davx5#578, #591)

* Soft fail sync on DeadObjectException so that it is retried without immediate error message

* Handle DeadObjectException (→ retries sync); Syncer: generalize all-catch
This commit is contained in:
Ricki Hirner 2024-05-09 13:30:22 +02:00
parent ad24dd54c7
commit 857309c451
6 changed files with 49 additions and 43 deletions

View file

@ -58,27 +58,21 @@ class AddressBookSyncer(
provider: ContentProviderClient, // for noop address book provider (not for contacts provider)
syncResult: SyncResult
) {
try {
if (updateLocalAddressBooks(account, syncResult)) {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.use { contactsProvider ->
for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
Logger.log.info("Synchronizing address book $addressBookAccount")
syncAddresBook(
addressBookAccount,
extras,
ContactsContract.AUTHORITY,
httpClient,
contactsProvider,
syncResult
)
}
if (updateLocalAddressBooks(account, syncResult)) {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.use { contactsProvider ->
for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
Logger.log.info("Synchronizing address book $addressBookAccount")
syncAddresBook(
addressBookAccount,
extras,
ContactsContract.AUTHORITY,
httpClient,
contactsProvider,
syncResult
)
}
}
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
}
Logger.log.info("Address book sync complete")
}
private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {

View file

@ -33,26 +33,21 @@ class CalendarSyncer(context: Context): Syncer(context) {
provider: ContentProviderClient,
syncResult: SyncResult
) {
try {
val accountSettings = AccountSettings(context, account)
val accountSettings = AccountSettings(context, account)
if (accountSettings.getEventColors())
AndroidCalendar.insertColors(provider, account)
else
AndroidCalendar.removeColors(provider, account)
if (accountSettings.getEventColors())
AndroidCalendar.insertColors(provider, account)
else
AndroidCalendar.removeColors(provider, account)
updateLocalCalendars(provider, account, accountSettings)
updateLocalCalendars(provider, account, accountSettings)
val calendars = AndroidCalendar
.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
for (calendar in calendars) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).performSync()
}
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
val calendars = AndroidCalendar
.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
for (calendar in calendars) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).performSync()
}
Logger.log.info("Calendar sync complete")
}
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {

View file

@ -64,10 +64,7 @@ class JtxSyncer(context: Context): Syncer(context) {
} catch (e: TaskProvider.ProviderTooOldException) {
TaskUtils.notifyProviderTooOld(context, e)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync jtx collections", e)
}
Logger.log.info("jtx sync complete")
}
private fun updateLocalCollections(account: Account, client: ContentProviderClient, settings: AccountSettings) {

View file

@ -11,6 +11,7 @@ import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.net.Uri
import android.os.DeadObjectException
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.ContactsContract
@ -296,7 +297,12 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}, { e, local, remote ->
when (e) {
// sync was cancelled or account has been removed: re-throw to SyncAdapterService (now BaseSyncer)
// DeadObjectException (may occur when syncing takes too long and process is demoted to cached):
// re-throw to base Syncer → will cause soft error and restart the sync process
is DeadObjectException ->
throw e
// sync was cancelled or account has been removed: re-throw to BaseSyncer
is InterruptedException,
is InterruptedIOException,
is InvalidAccountException ->

View file

@ -8,6 +8,7 @@ import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.DeadObjectException
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
@ -54,11 +55,11 @@ abstract class Syncer(val context: Context) {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncAdapterEntryPoint {
interface SyncerEntryPoint {
fun appDatabase(): AppDatabase
}
private val syncAdapterEntryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
private val syncAdapterEntryPoint = EntryPointAccessors.fromApplication(context, SyncerEntryPoint::class.java)
internal val db = syncAdapterEntryPoint.appDatabase()
@ -77,11 +78,24 @@ abstract class Syncer(val context: Context) {
val httpClient = lazy { HttpClient.Builder(context, accountSettings).build() }
try {
val runSync = true /* ose */
val runSync = /* ose */ true
if (runSync)
sync(account, extras, authority, httpClient, provider, syncResult)
} catch (e: DeadObjectException) {
/* May happen when the remote process dies or (since Android 14) when IPC (for instance with the calendar provider)
is suddenly forbidden because our sync process was demoted from a "service process" to a "cached process". */
Logger.log.log(Level.WARNING, "Received DeadObjectException, treating as soft error", e)
syncResult.stats.numIoExceptions++
} catch (e: InvalidAccountException) {
Logger.log.log(Level.WARNING, "Account was removed during synchronization", e)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync $authority", e)
syncResult.stats.numParseExceptions++
} finally {
if (httpClient.isInitialized())
httpClient.value.close()

View file

@ -27,8 +27,8 @@ androidx-work = "2.9.0"
appIntro = "7.0.0-beta02"
bitfire-cert4android = "f1cc9b9ca3"
bitfire-dav4jvm = "b87d772e44"
bitfire-ical4android = "f10bd57dac"
bitfire-vcard4android = "adf00bd98a"
bitfire-ical4android = "a3e886c738"
bitfire-vcard4android = "03a37a8284"
commons-collections = "4.4"
commons-lang = "3.14.0"
# don't update until API level 26 (Android 8) is the minimum API [https://github.com/bitfireAT/davx5/issues/130]