Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework sync exceptions based on new error codes. #1489

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ childA == childC
```

### Enhancements
* Fulltext queries now support prefix search by using the * operator, like `description TEXT 'alex*'`. (Core issue [#6860](https://github.com/realm/realm-core/issues/6860))
* Realm model classes now generate custom `toString`, `equals` and `hashCode` implementations. This makes it possible to compare by object reference across multiple collections. Note that two objects at different versions will not be considered equal, even
if the content is the same. Custom implementations of these methods will be respected if they are present. (Issue [#1097](https://github.com/realm/realm-kotlin/issues/1097))
* Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403))
Expand All @@ -46,7 +47,7 @@ if the content is the same. Custom implementations of these methods will be resp
* Minimum Android SDK: 16.

### Internal
* None.
* Updated to Realm Core 13.19.0, commit ea7c5d5e2900b8411a295aea3d1aa56aa55fff1d.


## 1.10.2 (2023-07-21)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,29 @@

package io.realm.kotlin.test.sync

import io.realm.kotlin.internal.interop.CategoryFlags
import io.realm.kotlin.internal.interop.ErrorCategory
import io.realm.kotlin.internal.interop.ErrorCode
import io.realm.kotlin.internal.interop.realm_auth_provider_e
import io.realm.kotlin.internal.interop.realm_errno_e
import io.realm.kotlin.internal.interop.realm_error_category_e
import io.realm.kotlin.internal.interop.realm_sync_client_metadata_mode_e
import io.realm.kotlin.internal.interop.realm_sync_connection_state_e
import io.realm.kotlin.internal.interop.realm_sync_errno_client_e
import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e
import io.realm.kotlin.internal.interop.realm_sync_errno_session_e
import io.realm.kotlin.internal.interop.realm_sync_error_category_e
import io.realm.kotlin.internal.interop.realm_sync_session_resync_mode_e
import io.realm.kotlin.internal.interop.realm_sync_session_state_e
import io.realm.kotlin.internal.interop.realm_user_state_e
import io.realm.kotlin.internal.interop.realm_web_socket_errno_e
import io.realm.kotlin.internal.interop.sync.AuthProvider
import io.realm.kotlin.internal.interop.sync.CoreConnectionState
import io.realm.kotlin.internal.interop.sync.CoreSyncSessionState
import io.realm.kotlin.internal.interop.sync.CoreUserState
import io.realm.kotlin.internal.interop.sync.MetadataMode
import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode
import io.realm.kotlin.internal.interop.sync.ProtocolConnectionErrorCode
import io.realm.kotlin.internal.interop.sync.ProtocolSessionErrorCode
import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory
import io.realm.kotlin.internal.interop.sync.SyncConnectionErrorCode
import io.realm.kotlin.internal.interop.sync.SyncSessionErrorCode
import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode
import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode
import org.junit.Test
import kotlin.reflect.KClass
import kotlin.test.BeforeTest
Expand All @@ -59,10 +58,11 @@ class SyncEnumTests {
}

@Test
fun appErrorCategory() {
fun errorCategory() {
checkEnum(realm_error_category_e::class) { nativeValue ->
ErrorCategory.of(nativeValue)
}
assertEquals(ErrorCategory.values().size, CategoryFlags.CATEGORY_ORDER.size)
}

@Test
Expand Down Expand Up @@ -94,30 +94,23 @@ class SyncEnumTests {
}

@Test
fun protocolClientErrorCode() {
checkEnum(realm_sync_errno_client_e::class) { nativeValue ->
ProtocolClientErrorCode.of(nativeValue)
}
}

@Test
fun protocolConnectionErrorCode() {
fun syncConnectionErrorCode() {
checkEnum(realm_sync_errno_connection_e::class) { nativeValue ->
ProtocolConnectionErrorCode.of(nativeValue)
SyncConnectionErrorCode.of(nativeValue)
}
}

@Test
fun protocolSessionErrorCode() {
fun syncSessionErrorCode() {
checkEnum(realm_sync_errno_session_e::class) { nativeValue ->
ProtocolSessionErrorCode.of(nativeValue)
SyncSessionErrorCode.of(nativeValue)
}
}

@Test
fun syncErrorCodeCategory() {
checkEnum(realm_sync_error_category_e::class) { nativeValue ->
SyncErrorCodeCategory.of(nativeValue)
fun websocketErrorCode() {
checkEnum(realm_web_socket_errno_e::class) { nativeValue ->
WebsocketErrorCode.of(nativeValue)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop
import io.realm.kotlin.internal.interop.sync.AppError
import io.realm.kotlin.internal.interop.sync.CoreSubscriptionSetState
import io.realm.kotlin.internal.interop.sync.SyncError
import io.realm.kotlin.internal.interop.sync.SyncErrorCode

// TODO Could be replace by lambda. See realm_app_config_new networkTransportFactory for example.
interface Callback<T : RealmNativePointer> {
Expand All @@ -39,7 +38,7 @@ fun interface SyncErrorCallback {

// Interface exposed towards `library-sync`
interface SyncSessionTransferCompletionCallback {
fun invoke(error: SyncErrorCode?)
fun invoke(error: CoreError?)
}

interface LogCallback {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.realm.kotlin.internal.interop

/**
* Wrapper for C-API `realm_error_t`.
* See https://github.com/realm/realm-core/blob/master/src/realm.h#L231
*/
class CoreError(
categoriesNativeValue: Int,
val errorCodeNativeValue: Int,
messageNativeValue: String?,
// These are represent in the C-API, but not populated by Core.
// path: String,
// userError: Throwable?
) {
val categories: CategoryFlags = CategoryFlags((categoriesNativeValue))
val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue)
val message = messageNativeValue

operator fun contains(category: ErrorCategory): Boolean = category in categories
}

data class CategoryFlags(val categoryFlags: Int) {

companion object {
/**
* See error code mapping to categories here:
* https://github.com/realm/realm-core/blob/master/src/realm/error_codes.cpp#L29
*
* In most cases, only 1 category is assigned, but some errors have multiple. So instead of
* overwhelming the user with many categories, we only select the most important to show
* in the error message. "important" is of course tricky to define, but generally
* we consider vague categories like [ErrorCategory.RLM_ERR_CAT_RUNTIME] as less important
* than more specific ones like [ErrorCategory.RLM_ERR_CAT_JSON_ERROR].
*
* In the current implementation, categories between index 0 and 7 are considered equal
* and the order is somewhat arbitrary. No error codes has multiple of these categories
* associated either.
*/
val CATEGORY_ORDER: List<ErrorCategory> = listOf(
ErrorCategory.RLM_ERR_CAT_CUSTOM_ERROR,
ErrorCategory.RLM_ERR_CAT_WEBSOCKET_ERROR,
ErrorCategory.RLM_ERR_CAT_SYNC_ERROR,
ErrorCategory.RLM_ERR_CAT_SERVICE_ERROR,
ErrorCategory.RLM_ERR_CAT_JSON_ERROR,
ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR,
ErrorCategory.RLM_ERR_CAT_SYSTEM_ERROR,
ErrorCategory.RLM_ERR_CAT_FILE_ACCESS,
ErrorCategory.RLM_ERR_CAT_HTTP_ERROR,
ErrorCategory.RLM_ERR_CAT_INVALID_ARG,
ErrorCategory.RLM_ERR_CAT_APP_ERROR,
ErrorCategory.RLM_ERR_CAT_LOGIC,
ErrorCategory.RLM_ERR_CAT_RUNTIME,
)
}

/**
* Returns a description of the most important category defined in [categoryFlags].
* If no known categories are found, the integer values for all the categories is returned
* as debugging information.
*/
val description: String = CATEGORY_ORDER.firstOrNull { category ->
category in this
}?.description ?: "$categoryFlags"

/**
* Check whether a given [ErrorCategory] is included in the [categoryFlags].
*/
operator fun contains(category: ErrorCategory): Boolean = (categoryFlags and category.nativeValue) != 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,21 @@ object CoreErrorConverter {
path: String?,
userError: Throwable?
): Throwable {
val categories: CategoryFlag = CategoryFlag(categoriesNativeValue)
val categories: CategoryFlags = CategoryFlags(categoriesNativeValue)
val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue)
val message: String = "[$errorCode]: $messageNativeValue"

return userError ?: when {
ErrorCode.RLM_ERR_INDEX_OUT_OF_BOUNDS == errorCode ->
IndexOutOfBoundsException(message)
ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories ->
ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories && ErrorCategory.RLM_ERR_CAT_SYNC_ERROR !in categories -> {
// Some sync errors flagged as both logical and illegal. In our case, we consider those
// IllegalState, so discard them them here and let them fall through to the bottom case
IllegalArgumentException(message)
}
ErrorCategory.RLM_ERR_CAT_LOGIC in categories || ErrorCategory.RLM_ERR_CAT_RUNTIME in categories ->
IllegalStateException(message)
else -> Error(message) // This can happen when propagating user level exceptions.
}
}

data class CategoryFlag(val categoryCode: Int) {
operator fun contains(other: ErrorCategory): Boolean = categoryCode and other.nativeValue != 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ expect enum class ErrorCategory : CodeDescription {
RLM_ERR_CAT_SERVICE_ERROR,
RLM_ERR_CAT_HTTP_ERROR,
RLM_ERR_CAT_CUSTOM_ERROR,
RLM_ERR_CAT_WEBSOCKET_ERROR;
RLM_ERR_CAT_WEBSOCKET_ERROR,
RLM_ERR_CAT_SYNC_ERROR;

companion object {
internal fun of(nativeValue: Int): ErrorCategory?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ expect enum class ErrorCode : CodeDescription {
RLM_ERR_SCHEMA_VERSION_MISMATCH,
RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE,
RLM_ERR_OPERATION_ABORTED,
RLM_ERR_AUTO_CLIENT_RESET_FAILED,
RLM_ERR_BAD_SYNC_PARTITION_VALUE,
RLM_ERR_CONNECTION_CLOSED,
RLM_ERR_INVALID_SUBSCRIPTION_QUERY,
RLM_ERR_SYNC_CLIENT_RESET_REQUIRED,
RLM_ERR_SYNC_COMPENSATING_WRITE,
RLM_ERR_SYNC_CONNECT_FAILED,
RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE,
RLM_ERR_SYNC_PERMISSION_DENIED,
RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED,
RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED,
RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED,
RLM_ERR_SYNC_USER_MISMATCH,
RLM_ERR_TLS_HANDSHAKE_FAILED,
RLM_ERR_WRONG_SYNC_TYPE,
RLM_ERR_SYNC_WRITE_NOT_ALLOWED,
RLM_ERR_SYSTEM_ERROR,
RLM_ERR_LOGIC,
RLM_ERR_NOT_SUPPORTED,
Expand Down Expand Up @@ -162,9 +178,7 @@ expect enum class ErrorCode : CodeDescription {
RLM_ERR_MAINTENANCE_IN_PROGRESS,
RLM_ERR_USERPASS_TOKEN_INVALID,
RLM_ERR_INVALID_SERVER_RESPONSE,
RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR,
RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR,
RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR,
REALM_ERR_APP_SERVER_ERROR,
RLM_ERR_CALLBACK,
RLM_ERR_UNKNOWN;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ import io.realm.kotlin.internal.interop.sync.CoreUserState
import io.realm.kotlin.internal.interop.sync.MetadataMode
import io.realm.kotlin.internal.interop.sync.NetworkTransport
import io.realm.kotlin.internal.interop.sync.ProgressDirection
import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode
import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory
import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode
import io.realm.kotlin.internal.interop.sync.SyncUserIdentity
import kotlinx.coroutines.CoroutineDispatcher
Expand Down Expand Up @@ -127,8 +125,6 @@ typealias RealmMutableSubscriptionSetPointer = NativePointer<RealmMutableSubscri
@Suppress("LongParameterList")
class SyncConnectionParams(
sdkVersion: String,
localAppName: String?,
localAppVersion: String?,
bundleId: String,
platformVersion: String,
device: String,
Expand All @@ -137,8 +133,6 @@ class SyncConnectionParams(
frameworkVersion: String
) {
val sdkName = "Kotlin"
val localAppName: String?
val localAppVersion: String?
val bundleId: String
val sdkVersion: String
val platformVersion: String
Expand All @@ -156,8 +150,6 @@ class SyncConnectionParams(
init {
this.sdkVersion = sdkVersion
this.bundleId = bundleId
this.localAppName = localAppName
this.localAppVersion = localAppVersion
this.platformVersion = platformVersion
this.device = device
this.deviceVersion = deviceVersion
Expand Down Expand Up @@ -624,8 +616,7 @@ expect object RealmInterop {
fun realm_sync_session_resume(syncSession: RealmSyncSessionPointer)
fun realm_sync_session_handle_error_for_testing(
syncSession: RealmSyncSessionPointer,
errorCode: ProtocolClientErrorCode,
category: SyncErrorCodeCategory,
error: ErrorCode,
errorMessage: String,
isFatal: Boolean
)
Expand Down
Loading