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

Add support for compensating writes #3259

Merged
merged 6 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Added `SyncConfiguration.CancelAsyncOperationsOnNonFatalErrors` which controls whether async operations (such as `Realm.GetInstanceAsync`, `Session.WaitForUploadAsync` and so on) should throw an exception whenever a non-fatal session error occurs. (Issue [#3222](https://github.com/realm/realm-dotnet/issues/3222))
* Added `AppConfiguration.SyncTimeoutOptions` which has a handful of properties that control sync timeouts, such as the connection timeout, ping-pong intervals, and others. (Issue [#3223](https://github.com/realm/realm-dotnet/issues/3223))
* Updated some of the exceptions being thrown by the SDK to align them better with system exceptions and include more information - for example, we'll now throw `ArgumentException` when invalid arguments are provided rather than `RealmException`. (Issue [#2796](https://github.com/realm/realm-dotnet/issues/2796))
* Added a new exception - `CompensatingWriteException` that contains information about the writes that have been reverted by the server due to permissions. It will be passed to the supplied `FlexibleSyncConfiguration.OnSessionError` callback similarly to other session errors. (Issue [#3258](https://github.com/realm/realm-dotnet/issues/3258))

### Fixed
* Changed the way the Realm SDK registers BsonSerializers. Previously, it would indiscriminately register them via `BsonSerializer.RegisterSerializer`, which would conflict if your app was using the `MongoDB.Bson` package and defined its own serializers for `DateTimeOffset`, `decimal`, or `Guid`. Now, registration happens via `BsonSerializer.RegisterSerializationProvider`, which means that the default serializers used by the SDK can be overriden by calling `BsonSerializer.RegisterSerializer` at any point before a serializer is instantiated or by calling `BsonSerializer.RegisterSerializationProvider` after creating an App/opening a Realm. (Issue [#3225](https://github.com/realm/realm-dotnet/issues/3225))
Expand Down
79 changes: 79 additions & 0 deletions Realm/Realm/Exceptions/CompensatingWriteException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Realms.Sync.Exceptions
{
/// <summary>
/// An exception class that indicates that one more object changes have been reverted
/// by the server.
/// </summary>
/// <remarks>
/// The two typical cases in which the server will revert a client write are:
/// 1. The client created an object that doesn't match any <see cref="Realm.Subscriptions"/>.
/// 2. The client created/updated an object it didn't have permissions to.
/// </remarks>
public class CompensatingWriteException : SessionException
{
/// <summary>
/// Gets a list of the compensating writes performed by the server.
/// </summary>
/// <value>The compensating writes performed by the server.</value>
public IEnumerable<CompensatingWriteInfo> CompensatingWrites { get; }

internal CompensatingWriteException(string message, IEnumerable<CompensatingWriteInfo> compensatingWrites)
: base(message, ErrorCode.CompensatingWrite)
{
CompensatingWrites = compensatingWrites;
}
}

/// <summary>
/// A class containing the details for a compensating write performed by the server.
/// </summary>
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is closely related to CompensatingWriteException")]
public class CompensatingWriteInfo
{
/// <summary>
/// Gets the type of the object which was affected by the compensating write.
/// </summary>
/// <value>The object type.</value>
public string ObjectType { get; }

/// <summary>
/// Gets the reason for the server to perform a compensating write.
/// </summary>
/// <value>The compensating write reason.</value>
public string Reason { get; }

/// <summary>
/// Gets the primary key of the object which was affected by the compensating write.
/// </summary>
/// <value>The object primary key.</value>
public RealmValue PrimaryKey { get; }

internal CompensatingWriteInfo(string objectName, string reason, RealmValue primaryKey)
{
ObjectType = objectName;
Reason = reason;
PrimaryKey = primaryKey;
}
}
}
37 changes: 23 additions & 14 deletions Realm/Realm/Handles/SessionHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Realms.Exceptions;
Expand All @@ -27,6 +28,7 @@
using Realms.Sync.ErrorHandling;
using Realms.Sync.Exceptions;
using Realms.Sync.Native;
using CompensatingWriteInfo = Realms.Sync.Exceptions.CompensatingWriteInfo;
fealebenpae marked this conversation as resolved.
Show resolved Hide resolved

namespace Realms.Sync
{
Expand All @@ -36,11 +38,7 @@ private static class NativeMethods
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void SessionErrorCallback(IntPtr session_handle_ptr,
ErrorCode error_code,
PrimitiveValue message,
IntPtr user_info_pairs,
IntPtr user_info_pairs_len,
[MarshalAs(UnmanagedType.U1)] bool is_client_reset,
SyncError error,
IntPtr managed_sync_config_handle);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
Expand Down Expand Up @@ -270,26 +268,27 @@ public override void Unbind()
}

[MonoPInvokeCallback(typeof(NativeMethods.SessionErrorCallback))]
private static void HandleSessionError(IntPtr sessionHandlePtr, ErrorCode errorCode, PrimitiveValue message, IntPtr userInfoPairs, IntPtr userInfoPairsLength, bool isClientReset, IntPtr managedSyncConfigurationBaseHandle)
private static void HandleSessionError(IntPtr sessionHandlePtr, SyncError error, IntPtr managedSyncConfigurationBaseHandle)
{
try
{
// Filter out end of input, which the client seems to have started reporting
if (errorCode == (ErrorCode)1)
if (error.error_code == (ErrorCode)1)
{
return;
}

using var handle = new SessionHandle(null, sessionHandlePtr);
var session = new Session(handle);
var messageString = message.AsString();
var messageString = error.message.AsString();
var logUrlString = error.log_url.AsString();
var syncConfigHandle = GCHandle.FromIntPtr(managedSyncConfigurationBaseHandle);
var syncConfig = (SyncConfigurationBase)syncConfigHandle.Target;

if (isClientReset)
if (error.is_client_reset)
{
var userInfo = StringStringPair.UnmarshalDictionary(userInfoPairs, userInfoPairsLength.ToInt32());
var clientResetEx = new ClientResetException(session.User.App, messageString, errorCode, userInfo);
var userInfo = StringStringPair.UnmarshalDictionary(error.user_info_pairs.Items, (int)error.user_info_pairs.Count);
var clientResetEx = new ClientResetException(session.User.App, messageString, error.error_code, userInfo);

if (syncConfig.ClientResetHandler.ClientResetMode != ClientResyncMode.Manual ||
syncConfig.ClientResetHandler.ManualClientReset != null)
Expand All @@ -305,18 +304,28 @@ private static void HandleSessionError(IntPtr sessionHandlePtr, ErrorCode errorC
}

SessionException exception;
if (errorCode == ErrorCode.PermissionDenied)
if (error.error_code == ErrorCode.PermissionDenied)
{
var userInfo = StringStringPair.UnmarshalDictionary(userInfoPairs, userInfoPairsLength.ToInt32());
var userInfo = error.user_info_pairs.AsEnumerable().ToDictionary(p => p.Key, p => p.Value);
#pragma warning disable CS0618 // Type or member is obsolete
exception = new PermissionDeniedException(session.User.App, messageString, userInfo);
#pragma warning restore CS0618 // Type or member is obsolete
}
else if (error.error_code == ErrorCode.CompensatingWrite)
{
var compensatingWrites = error.compensating_writes
.AsEnumerable()
.Select(c => new CompensatingWriteInfo(c.object_name, c.reason, new RealmValue(c.primary_key)))
.ToArray();
exception = new CompensatingWriteException(messageString, compensatingWrites);
}
else
{
exception = new SessionException(messageString, errorCode);
exception = new SessionException(messageString, error.error_code);
}

exception.HelpLink = logUrlString;

if (syncConfig.OnSessionError != null)
{
syncConfig.OnSessionError?.Invoke(session, exception);
Expand Down
10 changes: 5 additions & 5 deletions Realm/Realm/Native/MarshaledVector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ namespace Realms
internal struct MarshaledVector<T>
where T : struct
{
private IntPtr items;
private IntPtr count;
public IntPtr Items;
public IntPtr Count;

internal IEnumerable<T> AsEnumerable()
{
return Enumerable.Range(0, (int)count).Select(MarshalElement);
return Enumerable.Range(0, (int)Count).Select(MarshalElement);
}

private unsafe T MarshalElement(int elementIndex)
{
var @struct = default(T);
Unsafe.CopyBlock(Unsafe.AsPointer(ref @struct), IntPtr.Add(items, elementIndex * Unsafe.SizeOf<T>()).ToPointer(), (uint)Unsafe.SizeOf<T>());
Unsafe.CopyBlock(Unsafe.AsPointer(ref @struct), IntPtr.Add(Items, elementIndex * Unsafe.SizeOf<T>()).ToPointer(), (uint)Unsafe.SizeOf<T>());
return @struct;
}
}
}
}
34 changes: 19 additions & 15 deletions Realm/Realm/Native/PrimitiveValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,44 +68,44 @@ internal unsafe struct PrimitiveValue
[MarshalAs(UnmanagedType.U1)]
public RealmValueType Type;

public static PrimitiveValue Null() => new PrimitiveValue
public static PrimitiveValue Null() => new()
{
Type = RealmValueType.Null,
};

public static PrimitiveValue Bool(bool value) => new PrimitiveValue
public static PrimitiveValue Bool(bool value) => new()
{
Type = RealmValueType.Bool,
int_value = value ? 1 : 0,
};

public static PrimitiveValue NullableBool(bool? value) => value.HasValue ? Bool(value.Value) : Null();

public static PrimitiveValue Int(long value) => new PrimitiveValue
public static PrimitiveValue Int(long value) => new()
{
Type = RealmValueType.Int,
int_value = value
};

public static PrimitiveValue NullableInt(long? value) => value.HasValue ? Int(value.Value) : Null();

public static PrimitiveValue Float(float value) => new PrimitiveValue
public static PrimitiveValue Float(float value) => new()
{
Type = RealmValueType.Float,
float_value = value
};

public static PrimitiveValue NullableFloat(float? value) => value.HasValue ? Float(value.Value) : Null();

public static PrimitiveValue Double(double value) => new PrimitiveValue
public static PrimitiveValue Double(double value) => new()
{
Type = RealmValueType.Double,
double_value = value
};

public static PrimitiveValue NullableDouble(double? value) => value.HasValue ? Double(value.Value) : Null();

public static PrimitiveValue Date(DateTimeOffset value) => new PrimitiveValue
public static PrimitiveValue Date(DateTimeOffset value) => new()
{
Type = RealmValueType.Date,
timestamp_value = new TimestampValue(value.ToUniversalTime().Ticks)
Expand Down Expand Up @@ -212,7 +212,7 @@ public static PrimitiveValue Object(ObjectHandle handle)

public double AsDouble() => double_value;

public DateTimeOffset AsDate() => new DateTimeOffset(timestamp_value.ToTicks(), TimeSpan.Zero);
public DateTimeOffset AsDate() => new(timestamp_value.ToTicks(), TimeSpan.Zero);

public Decimal128 AsDecimal() => Decimal128.FromIEEEBits(decimal_bits[1], decimal_bits[0]);

Expand Down Expand Up @@ -247,7 +247,7 @@ public string AsString()
return null;
}

return Encoding.UTF8.GetString(string_value.data, (int)string_value.size);
return string_value;
}

public byte[] AsBinary()
Expand Down Expand Up @@ -282,13 +282,6 @@ public IRealmObjectBase AsObject(Realm realm)
return realm.MakeObject(objectMetadata, handle);
}

[StructLayout(LayoutKind.Sequential)]
private unsafe struct StringValue
{
public byte* data;
public IntPtr size;
}

[StructLayout(LayoutKind.Sequential)]
private unsafe struct BinaryValue
{
Expand Down Expand Up @@ -323,4 +316,15 @@ public TimestampValue(long ticks)
public long ToTicks() => (seconds * TicksPerSecond) + (nanoseconds / NanosecondsPerTick) + UnixEpochTicks;
}
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct StringValue
{
public byte* data;
public IntPtr size;

public string AsString() => Encoding.UTF8.GetString(data, (int)size);

public static implicit operator string(StringValue value) => value.AsString();
}
}
49 changes: 49 additions & 0 deletions Realm/Realm/Native/SyncError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

using System.Runtime.InteropServices;
using Realms.Sync.Exceptions;
using Realms.Sync.Native;

namespace Realms.Native
{
[StructLayout(LayoutKind.Sequential)]
internal struct SyncError
{
public ErrorCode error_code;

public StringValue message;

public StringValue log_url;

[MarshalAs(UnmanagedType.U1)]
public bool is_client_reset;

public MarshaledVector<StringStringPair> user_info_pairs;

public MarshaledVector<CompensatingWriteInfo> compensating_writes;
}

[StructLayout(LayoutKind.Sequential)]
internal struct CompensatingWriteInfo
{
public StringValue reason;
public StringValue object_name;
public PrimitiveValue primary_key;
}
}
Loading