Skip to content

Commit d81500c

Browse files
authored
[RKOTLIN-1096] Add SyncException.isFatal to signal unrecoverable sync exceptions (#1800)
1 parent 4e7ea38 commit d81500c

File tree

8 files changed

+99
-38
lines changed

8 files changed

+99
-38
lines changed

.github/workflows/include-static-analysis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ jobs:
3333

3434
- name: Run Ktlint
3535
run: ./gradlew ktlintCheck
36-
continue-on-error: true
3736

3837
- name: Stash Ktlint results
3938
run: |
@@ -85,7 +84,6 @@ jobs:
8584

8685
- name: Run Detekt
8786
run: ./gradlew detekt
88-
continue-on-error: true
8987

9088
- name: Stash Detekt results
9189
run: |

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1782,4 +1782,4 @@ jobs:
17821782
secrets: inherit
17831783
with:
17841784
version-label: ${{ needs.check-cache.outputs.version-label }}
1785-
packages-sha-label: ${{ needs.check-cache.outputs.packages-sha }}
1785+
packages-sha-label: ${{ needs.check-cache.outputs.packages-sha }}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
### Enhancements
77
- Avoid exporting Core's symbols so we can statically build the Kotlin SDK with other SDKs like Swift in the same project. (Issue [JIRA](https://jira.mongodb.org/browse/RKOTLIN-877)).
88
- Improved mechanism for unpacking of JVM native libs suitable for local development. (Issue [#1715](https://github.com/realm/realm-kotlin/issues/1715) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1065)).
9+
* [Sync] Add `SyncException.isFatal` to signal fatal unrecoverable exceptions. (Issue [#1767](https://github.com/realm/realm-kotlin/issues/1767) [RKOTLIN-1096](https://jira.mongodb.org/browse/RKOTLIN-1096)).
910

1011
### Fixed
1112
- None.

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.realm.kotlin.mongodb.exceptions
1919

2020
import io.realm.kotlin.internal.asPrimitiveRealmAnyOrElse
2121
import io.realm.kotlin.internal.interop.sync.CoreCompensatingWriteInfo
22+
import io.realm.kotlin.mongodb.sync.SyncSession
2223
import io.realm.kotlin.types.RealmAny
2324

2425
/**
@@ -31,7 +32,19 @@ import io.realm.kotlin.types.RealmAny
3132
*
3233
* @see io.realm.kotlin.mongodb.sync.SyncConfiguration.Builder.errorHandler
3334
*/
34-
public open class SyncException internal constructor(message: String?) : AppException(message)
35+
public open class SyncException internal constructor(message: String?, isFatal: Boolean) : AppException(message) {
36+
/**
37+
* Flag to indicate that something has gone wrong with Device Sync in a way that is not
38+
* recoverable and [SyncSession] will be [SyncSession.State.INACTIVE] until this error is
39+
* resolved.
40+
*
41+
* It is still possible to use the Realm locally after receiving an error where this flag is
42+
* true. However, this must be done with caution as data written to the realm after this point
43+
* risk getting lost as many errors of this category will result in a Client Reset once the
44+
* client re-connects to the server.
45+
*/
46+
public val isFatal: Boolean = isFatal
47+
}
3548

3649
/**
3750
* Thrown when something has gone wrong with Device Sync in a way that is not recoverable.
@@ -47,30 +60,33 @@ public open class SyncException internal constructor(message: String?) : AppExce
4760
*
4861
* @see io.realm.kotlin.mongodb.sync.SyncConfiguration.Builder.errorHandler
4962
*/
50-
public class UnrecoverableSyncException internal constructor(message: String) :
51-
SyncException(message)
63+
@Deprecated("This will be removed in the future. Test for SyncException.isFatal instead.")
64+
public open class UnrecoverableSyncException internal constructor(message: String) :
65+
SyncException(message, true)
5266

5367
/**
5468
* Thrown when the type of sync used by the server does not match the one used by the client, i.e.
5569
* the server and client disagrees whether to use Partition-based or Flexible Sync.
5670
*/
57-
public class WrongSyncTypeException internal constructor(message: String) : SyncException(message)
71+
public class WrongSyncTypeException internal constructor(message: String) :
72+
UnrecoverableSyncException(message)
5873

5974
/**
6075
* Thrown when the server does not support one or more of the queries defined in the
6176
* [io.realm.kotlin.mongodb.sync.SubscriptionSet].
6277
*/
63-
public class BadFlexibleSyncQueryException internal constructor(message: String?) :
64-
SyncException(message)
78+
public class BadFlexibleSyncQueryException internal constructor(message: String?, isFatal: Boolean) :
79+
SyncException(message, isFatal)
6580

6681
/**
6782
* Thrown when the server undoes one or more client writes. Details on undone writes can be found in
6883
* [writes].
6984
*/
7085
public class CompensatingWriteException internal constructor(
7186
message: String,
72-
compensatingWrites: Array<CoreCompensatingWriteInfo>
73-
) : SyncException(message) {
87+
compensatingWrites: Array<CoreCompensatingWriteInfo>,
88+
isFatal: Boolean
89+
) : SyncException(message, isFatal) {
7490
/**
7591
* List of all the objects created that has been reversed as part of triggering this exception.
7692
*/

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal suspend fun <T : RealmObject> createSubscriptionFromQuery(
6969
realm.syncSession.downloadAllServerChanges()
7070
subscriptions.refresh()
7171
subscriptions.errorMessage?.let { errorMessage: String ->
72-
throw BadFlexibleSyncQueryException(errorMessage)
72+
throw BadFlexibleSyncQueryException(errorMessage, isFatal = false)
7373
}
7474
}
7575
// Rerun the query on the latest Realm version.

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -78,33 +78,32 @@ internal fun <T, R> channelResultCallback(
7878
internal fun convertSyncError(syncError: SyncError): SyncException {
7979
val errorCode = syncError.errorCode
8080
val message = createMessageFromSyncError(errorCode)
81-
return if (syncError.isFatal) {
82-
// An unrecoverable exception happened
83-
UnrecoverableSyncException(message)
84-
} else {
85-
when (errorCode.errorCode) {
86-
ErrorCode.RLM_ERR_WRONG_SYNC_TYPE -> WrongSyncTypeException(message)
81+
return when (errorCode.errorCode) {
82+
ErrorCode.RLM_ERR_WRONG_SYNC_TYPE -> WrongSyncTypeException(message)
8783

88-
ErrorCode.RLM_ERR_INVALID_SUBSCRIPTION_QUERY -> {
89-
// Flexible Sync Query was rejected by the server
90-
BadFlexibleSyncQueryException(message)
91-
}
84+
ErrorCode.RLM_ERR_INVALID_SUBSCRIPTION_QUERY -> {
85+
// Flexible Sync Query was rejected by the server
86+
BadFlexibleSyncQueryException(message, syncError.isFatal)
87+
}
9288

93-
ErrorCode.RLM_ERR_SYNC_COMPENSATING_WRITE -> CompensatingWriteException(
94-
message,
95-
syncError.compensatingWrites
96-
)
97-
ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED,
98-
ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED,
99-
ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> {
100-
// Permission denied errors should be unrecoverable according to Core, i.e. the
101-
// client will disconnect sync and transition to the "inactive" state
102-
UnrecoverableSyncException(message)
103-
}
104-
else -> {
105-
// An error happened we are not sure how to handle. Just report as a generic
106-
// SyncException.
107-
SyncException(message)
89+
ErrorCode.RLM_ERR_SYNC_COMPENSATING_WRITE -> CompensatingWriteException(
90+
message,
91+
syncError.compensatingWrites,
92+
syncError.isFatal
93+
)
94+
ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED,
95+
ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED,
96+
ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> {
97+
// Permission denied errors should be unrecoverable according to Core, i.e. the
98+
// client will disconnect sync and transition to the "inactive" state
99+
UnrecoverableSyncException(message)
100+
}
101+
else -> {
102+
// An error happened we are not sure how to handle. Just report as a generic
103+
// SyncException.
104+
when (syncError.isFatal) {
105+
false -> SyncException(message, syncError.isFatal)
106+
true -> UnrecoverableSyncException(message)
108107
}
109108
}
110109
}

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ internal class SubscriptionSetImpl<T : BaseRealm>(
127127
if (result) {
128128
return true
129129
} else {
130-
throw BadFlexibleSyncQueryException(errorMessage)
130+
throw BadFlexibleSyncQueryException(errorMessage, isFatal = false)
131131
}
132132
}
133133
else -> throw IllegalStateException("Unexpected value: $result")

packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import io.realm.kotlin.mongodb.User
4545
import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException
4646
import io.realm.kotlin.mongodb.exceptions.SyncException
4747
import io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException
48+
import io.realm.kotlin.mongodb.exceptions.WrongSyncTypeException
4849
import io.realm.kotlin.mongodb.internal.SyncSessionImpl
4950
import io.realm.kotlin.mongodb.subscriptions
5051
import io.realm.kotlin.mongodb.sync.InitialSubscriptionsCallback
@@ -370,7 +371,10 @@ class SyncedRealmTests {
370371
// Second
371372
channel.receiveOrFail().let { error ->
372373
assertNotNull(error.message)
374+
// Deprecated
373375
assertIs<UnrecoverableSyncException>(error)
376+
assertIs<SyncException>(error)
377+
assertTrue(error.isFatal)
374378
}
375379

376380
deferred.cancel()
@@ -417,6 +421,49 @@ class SyncedRealmTests {
417421
}
418422
}
419423

424+
@Test
425+
fun errorHandler_wrongSyncTypeException() {
426+
val channel = TestChannel<Throwable>()
427+
// Remove permissions to generate a sync error containing ONLY the original path
428+
// This way we assert we don't read wrong data from the user_info field
429+
val (email, password) = "test_nowrite_noread_${randomEmail()}" to "password1234"
430+
val user = runBlocking {
431+
app.createUserAndLogIn(email, password)
432+
}
433+
434+
// Opens FLX synced realm against a PBS app
435+
val config = SyncConfiguration.Builder(
436+
schema = setOf(ParentPk::class, ChildPk::class),
437+
user = user,
438+
).errorHandler { _, error ->
439+
channel.trySendOrFail(error)
440+
}.build()
441+
442+
runBlocking {
443+
val deferred = async {
444+
Realm.open(config).use {
445+
// Make sure that the test eventually fail. Coroutines can cancel a delay
446+
// so this doesn't always block the test for 10 seconds.
447+
delay(10_000)
448+
channel.send(AssertionError("Realm was successfully opened"))
449+
}
450+
}
451+
452+
val error = channel.receiveOrFail()
453+
val message = error.message
454+
assertNotNull(message)
455+
assertIs<WrongSyncTypeException>(error)
456+
assertTrue(error.isFatal)
457+
// Deprecated
458+
assertIs<UnrecoverableSyncException>(error)
459+
assertTrue(
460+
message.contains("Client connected using flexible sync when app is using partition-based sync"),
461+
"Was: $message"
462+
)
463+
deferred.cancel()
464+
}
465+
}
466+
420467
@Test
421468
fun testErrorHandler() {
422469
// Open a realm with a schema. Close it without doing anything else

0 commit comments

Comments
 (0)