realm / realm-dotnet

Realm is a mobile database: a replacement for SQLite & ORMs
https://realm.io
Apache License 2.0
1.25k stars 165 forks source link

Invalid Transaction Exception #2141

Closed renjop closed 3 years ago

renjop commented 3 years ago

I have an update to a RealmObject in a write transaction, but I always get a "Realms.Exceptions.RealmInvalidTransactionException: 'Cannot modify managed objects outside of a write transaction.'"

using (var trans = realm.BeginWrite())
{
      var convRef = realm.Find<Conversation>(conversation.ConversationId);
      convRef.TimeStatus = "relojGris.png"; //Exception raised here
      trans.Commit();
}

It is worth mentioning that this object is periodically on a background thread. I've tried doing the ThreadSafeReference.Create and resolving it and the problem still persists. I have this problem both on iOS and Android devices. I am using RealmVersion 5.1.2. Thanks again,

nirinchev commented 3 years ago

Sounds to me like you obtained a reference to the object from one instance of the Realm and are trying to modify it from another. Can you try adding var isSameRealm = ReferenceEquals(realm, convRef.Realm) to see if the Realm beginning the transaction is the instance that owns the conversation.

renjop commented 3 years ago

Hi nirichev, I added your suggestion and it constantly shows as False.

Thanks Again,

nirinchev commented 3 years ago

That indeed indicates that the object you're trying to modify doesn't belong to the Realm you're starting the transaction with, which I don't have an explanation for assuming that the code snippet you posted is the actual code being run. Can you share a simple project that reproduces it? In the issue description, you mention

It is worth mentioning that this object is periodically on a background thread

Can you clarify that a little? Since Realm instances/RealmObjects are thread confined, you cannot pass them between threads - if that's what's going on, it's possible that the Realm you're creating a transaction on is opened on thread A, while the RealmObject is tied to thread B, where there's no transaction open currently.

renjop commented 3 years ago

Hi, Let me explain a little. We are using web sockets in our application, when an event gets triggered by the web socket event, a new realm object is created and added to the database.

#Using the following code is where I want to update a single property in my conversation object depending on the business logic. 
Task.Factory.StartNew(() =>
            {
                while (!CancellationTokenSource.Token.IsCancellationRequested)
                {
                    Thread.Sleep(millisecondsTimeout: (int)TimeSpan.FromSeconds(5).TotalMilliseconds);
                    Device.BeginInvokeOnMainThread(() => // If I didn't execute it in the main thread it throws a different exception
                    {
                              # Get configuration is a method that manages the database migrations 
                              # returns a RealmConfigurationBase object
                               var realm = Realm.GetInstance(GetConfiguration());
                               using (var trans = realm.BeginWrite())
                              {
                                      var convRef = realm.Find<Conversation>(conversation.ConversationId);
                                     var isSamerealm = ReferenceEquals(realm, convRef);
                                      Console.WriteLine($"isSame Realm {isSamerealm}");
                                      convRef.TimeStatus = "relojGris.png"; //Exception raised here
                                      trans.Commit();
                             }
                    });
                }

            }, CancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

Even though the isSamerealm shows as False, the object does get updated.

On the Web socket class that is in charge of getting the events I am also using transactions using the same snippet:

using (var trans = realm.BeginWrite())
{
.... further business logic
trans.Commit();
}

Do you think it might be possible that when multiple transactions are being written (it would be weird considering that the purpose of a transaction is to lock any concurrent writes) raises the exception?

Let me check if I find a way of creating a simple project since our project is kind of big and has a lot of business specific logic, specially the web socket communications.

Thanks again,

nirinchev commented 3 years ago

Hm, looks like the check you're doing is var isSamerealm = ReferenceEquals(realm, convRef);, but it needs to compare the Realm property on convRef. That being said, your code looks perfectly legit and I don't see a reason why it would throw there. I would be very interested in seeing a repro for that one.

renjop commented 3 years ago

You're right, my mistake I didnt see the convRef.realm. Now it always show as True. Let me work on the repo and I'll drop it here when I have it done. Thanks again, Best Regards,

renjop commented 3 years ago

I was unable to replicate it on another project/solution, but it still persists on my project.

nirinchev commented 3 years ago

Is your project something that you'd be at liberty to privately share? We have a tool where you can securely upload files and I can take a look at it.

renjop commented 3 years ago

No, this project has proprietary code, thanks for your help.

nirinchev commented 3 years ago

Hm, fair enough - can you at least paste the exact code that throws the exception? I'm assuming that the snippet in https://github.com/realm/realm-dotnet/issues/2141#issuecomment-738860772 is simplified/reduced from the original code as it appears to be setting TimeStatus to some string every 5 seconds, which would be surprising.

If that's not possible either, what you could do is to add the following check:

// Verifies that the Realm that owns convRef is indeed in transaction
if (!convRef.Realm.IsInTransaction)
{
    throw new Exception("How did this happen?!");
}

// If we're here, convRef should definitely be writeable.
convRef.TimeStatus = "some-string";

And just to confirm, convRef is the only object being modified by your code, right? There's no custom setter of the TimeStatus property that goes ahead and modifies something else? I know it's far fetched, it's just extremely unusual that opening a transaction, looking up an object, then modifying it, would result in this exception. If you could paste the stacktrace, that could also possibly point to a potential culprit.

renjop commented 3 years ago

Hi, I initially had

using (var trans = realm.BeginWrite())
{
      var convRef = realm.Find<Conversation>(conversation.ConversationId);
      convRef.TimeStatus = "relojGris.png"; //Exception raised here
      trans.Commit();
}
realm.Refresh();

By removing the the using and realm.Refresh statements, the issue went away:

realm.Write(() =>{
conv.TimeStatus = "relojGris.png";
});

My Conversation model is defined as:

public class Conversation : RealmObject
    {
        [PrimaryKey]
        public long ConversationId { get; set; }
        public int SocialNetworkId { get; set; }
        public int CustomerFileId { get; set; }
        public int CustomerTypeId { get; set; }
        public int SkillId { get; set; }
        public string SkillName { get; set; }
        public string CustomerTypeName { get; set; }
        public long ConversationDate { get; set; }
        public string SocialNetwortIdentifier { get; set; }
        public string SocialNetworkIcon { get; set; }
        public long SocialNetworkBotId { get; set; }
        public string ConversationBlockedByUsername { get; set; }
        public string ClientName { get; set; }
        public long ConversationState { get; set; }
        public int ConversationOrder { get; set; }
        public string TimeStatus { get; set; }
        public string UnreadStatus { get; set; }
        public bool IsBlocked { get; set; } = false;
        public string ConversationStatusText { get; set; }
        public IList<Message> Messages { get; }
        public string ReferencedConversations { get; set; }
        public int BusinessId { get; set; }
        public long NextConversationId { get; set; }
        public Message LastMessageSent { get; set; }
        public override string ToString()
        {
            return string.Format("ConversationId: {0}", ConversationId);
        }

    }

The stack trace is attached. stack_trace.txt

Thanks again, Best regards,

nirinchev commented 3 years ago

Hm... this is very interesting - the stacktrace points to an update happening from the UI:

  at TalkMe.Models.Realm.Conversation.set_TimeStatus (System.String value) [0x00011] in D:\Consystec\Visual Studio 2019\TalkMeMovil-Realm\TalkMe\TalkMe\Models\Realm\Conversation.cs:25 
  at TalkMe.Views.Cells.ConversationViewCell.<InitializeComponent>typedBindingsM__40 (TalkMe.Models.Realm.Conversation , System.String ) <0x78a27818 + 0x0003f> in <62dac39e275b429383a7c2a89c15e3dd>:0 
  at Xamarin.Forms.Internals.TypedBinding`2[TSource,TProperty].ApplyCore (System.Object sourceObject, Xamarin.Forms.BindableObject target, Xamarin.Forms.BindableProperty property, System.Boolean fromTarget) [0x0019f] in D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:229 

My guess is that for some reason the binding engine sees the change after the transaction is committed, then tries to reapply the bindings, but for some reason, as part of this process, decides to invoke the setter of the Conversation.TimeStatus. This is happening outside of the write transaction (which just got committed), which is why the exception is being thrown. I don't have a good guess why XF decides to set a property after an update coming from the view model. Are you by any chance using two-way compiled data bindings?

renjop commented 3 years ago

Hi, On the ViewCell in mention (ConversationViewCell) I only have a One-Way binding:

<imageCircle:CircleImage
            Grid.Column="0"
            WidthRequest="30"
            HeightRequest="30"
            MinimumWidthRequest="50"
            MinimumHeightRequest="50"
            Margin="10"
            Source="{Binding TimeStatus}"
            VerticalOptions="Center">
        </imageCircle:CircleImage>
nirinchev commented 3 years ago

The stacktrace points to ApplyCore in TypedBinding.cs:229. Looking at the TypedBinding source code (for 4.8.0 - not sure which XF version you're on, but that version seemed to align with the other numbers in the stacktrace):

https://github.com/xamarin/Xamarin.Forms/blob/3fdffc05d5175bc3b7e519e7726f1ad70cb5b160/Xamarin.Forms.Core/TypedBinding.cs#L222-L230

Line 229 is indeed XF invoking the setter of the property. On line 222, needsSetter is set to true only when mode is BindingMode.TwoWay or BindingMode.OneWayToSource. My guess is that, the binding engine incorrectly assumes that your binding is two-way. As far as I can tell, the Source property on Image from which CircleImage I assume derives is OneWay, so I don't know what could be causing XF to attempt to invoke the setter.

As far as I can tell, you got it working by using realm.Write and removing the explicit refresh. Not sure if you feel it's worth spending more time on this issue, but you could also try explicitly setting the mode of the Source binding to OneWay and see if that makes a difference.

In any case, the investigation so far seems to point to a bug/peculiarity of the XF data binding engine and not a problem within Realm itself. Combined with the fact that you have a workaround, do you think we can close the issue?

renjop commented 3 years ago

Sure thing, I appreciate all your help. I am using XForms 4.8.0.1687. It did help removing the realm.Write() and realm.Refresh(). Thanks again, Best Regards,

nirinchev commented 3 years ago

No problem, happy to see it resolved and thanks for your patience getting to the bottom of this!