NHibernate.StaleObjectStateException: Saga crashes in NHibernate Persister

303 views
Skip to first unread message

Morten

unread,
Sep 9, 2015, 9:56:56 AM9/9/15
to Particular Software
Product name: NServiceBus NHibernate persistence
Version: 6.1.0
Stacktrace: See below

I am getting some exceptions which I don't know how to fix. I am using NHibernate persistence.

I don't know which parts of this is important, so I will try to describe my scenario fairly detailed.

I have a Saga for processing incoming "orders". An order is an instruction from a workflow to go out to an external service and do stuff and report back the result. When the result is reported back to the workflow, zero or more follow up orders will be triggered. A follow up order means that a new order is created and sent locally to the same handler with the same saga mapping ID as the initial order. This continues until there are no more follow up orders. Well, that was the intention anyway. What happens now is that processing is OK for the first order and if there are no follow up orders on the first one, life is good. If there are follow up orders the first order fails and ends up as a red message in ServiceInsight.

I found an old issue here (https://github.com/Particular/NServiceBus.NHibernate/issues/20) that contains the same exception, but that was a problem that was fixed in 4.4.0.

Any help is appreciated. Thanks.

Morten


Stack trace:

NHibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [CustomerDirect.OrderService.ProcessOrderHandlerSagaData#109db8ae-3c84-4dcc-9659-a50e00b363e3]
   at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
   at NHibernate.Persister.Entity.AbstractEntityPersister.UpdateOrInsert(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
   at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Int32[] dirtyFields, Boolean hasDirtyCollection, Object[] oldFields, Object oldVersion, Object obj, Object rowId, ISessionImplementor session)
   at NHibernate.Action.EntityUpdateAction.Execute()
   at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
   at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
   at NHibernate.Engine.ActionQueue.ExecuteActions()
   at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
   at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
   at NHibernate.Impl.SessionImpl.Flush()
   at NServiceBus.Persistence.NHibernate.OpenSessionBehavior.InnerInvoke(BehaviorContext context, Action next, Func`1 connectionRetriever) in c:\BuildAgent\work\5135de308b2f3016\src\NServiceBus.NHibernate\SharedSession\OpenSessionBehavior.cs:line 75
   at NServiceBus.Persistence.NHibernate.OpenSessionBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\5135de308b2f3016\src\NServiceBus.NHibernate\SharedSession\OpenSessionBehavior.cs:line 35
   at NServiceBus.SetCurrentMessageBeingHandledBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Behaviors\SetCurrentMessageBeingHandledBehavior.cs:line 17
   at NServiceBus.LoadHandlersBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Behaviors\LoadHandlersBehavior.cs:line 46
   at NServiceBus.ApplyIncomingMessageMutatorsBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\MessageMutator\ApplyIncomingMessageMutatorsBehavior.cs:line 23
   at NServiceBus.ExecuteLogicalMessagesBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Messages\ExecuteLogicalMessagesBehavior.cs:line 24
   at NServiceBus.CallbackInvocationBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Behaviors\CallbackInvocationBehavior.cs:line 22
   at NServiceBus.DeserializeLogicalMessagesBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Messages\DeserializeLogicalMessagesBehavior.cs:line 47
   at NServiceBus.ApplyIncomingTransportMessageMutatorsBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\MessageMutator\ApplyIncomingTransportMessageMutatorsBehavior.cs:line 20
   at NServiceBus.SubscriptionReceiverBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Subscriptions\MessageDrivenSubscriptions\SubscriptionReceiverBehavior.cs:line 32
   at NServiceBus.UnitOfWorkBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\UnitOfWork\UnitOfWorkBehavior.cs:line 42
   at NServiceBus.Persistence.NHibernate.OpenSqlConnectionBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\5135de308b2f3016\src\NServiceBus.NHibernate\SharedSession\OpenSqlConnectionBehavior.cs:line 38
   at NServiceBus.ChildContainerBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Behaviors\ChildContainerBehavior.cs:line 17
   at NServiceBus.ProcessingStatisticsBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Monitoring\ProcessingStatisticsBehavior.cs:line 23
   at NServiceBus.AuditBehavior.Invoke(IncomingContext context, Action next) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Audit\AuditBehavior.cs:line 20
   at NServiceBus.Pipeline.PipelineExecutor.Execute[T](BehaviorChain`1 pipelineAction, T context) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Pipeline\PipelineExecutor.cs:line 127
   at NServiceBus.Unicast.Transport.TransportReceiver.ProcessMessage(TransportMessage message) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Transport\TransportReceiver.cs:line 344
   at NServiceBus.Unicast.Transport.TransportReceiver.TryProcess(TransportMessage message) in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Unicast\Transport\TransportReceiver.cs:line 230
   at NServiceBus.Transports.Msmq.MsmqDequeueStrategy.Action() in c:\BuildAgent\work\1b05a2fea6e4cd32\src\NServiceBus.Core\Transports\Msmq\MsmqDequeueStrategy.cs:line 266

Szymon Pobiega

unread,
Sep 10, 2015, 6:40:39 AM9/10/15
to particula...@googlegroups.com
Hi Morten

Can you share the saga code? Are you overriding the NH mappings for this saga in any way? Are the saga data rows accessed from outside of a saga? The default behavior of NH persister is to use optimistic concurrency based on all field values of the saga data entity so if something else changed any of the saga data entity columns while the saga was loaded for processing, the update will fail (resulting in 0 updated rows) and NH will throw the exception you got.

Thanks,
Szymon

--
You received this message because you are subscribed to the Google Groups "Particular Software" group.
To unsubscribe from this group and stop receiving emails from it, send an email to particularsoftw...@googlegroups.com.
To post to this group, send email to particula...@googlegroups.com.
Visit this group at http://groups.google.com/group/particularsoftware.
To view this discussion on the web visit https://groups.google.com/d/msgid/particularsoftware/07184861-7342-499e-afa0-31502e091ba1%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Morten Petteroe

unread,
Sep 10, 2015, 6:50:23 AM9/10/15
to particula...@googlegroups.com

Hi Szymon,

 

The Saga code is below. I am not overriding any NHibernate mappings, everything is out of the box NServiceBus.Nhibernate Nuget stuff. The Saga data is only accessed by NServiceBus. Nothing else is using that database.

 

Here is the Saga code:

 

    public partial class ProcessOrderHandler :

        IHandleMessages<OrderProcessed>,

        IHandleTimeouts<ProcessOrderTimeout>,

        IHandleTimeouts<ProcessOrderLater>

    {

        ILog _logger;

 

        [Dependency]

        public IOrderMapper OrderMapper { get; set; }

 

        [Dependency]

        public IOrderRepository OrderRepository { get; set; }

 

        [Dependency]

        public IConfigurationRepository ConfigurationRepository { get; set; }

 

        public ProcessOrderHandler()

        {

            _logger = LogManager.GetLogger(GetType());

        }

 

        partial void HandleImplementation(ProcessOrder message)

        {

            _logger.DebugFormat("OrderService received {0} message with order id {0}/order type {1} ", message.GetType().Name, message.OrderID, message.OrderTypeCode ?? "not set");

 

            var processMessage = message;

            try

            {

                var config = ConfigurationRepository.GetConfiguration(ConfigurationRepository.GetAppsetting(Constants.APPSETTINGS_KEY_CONFIGURATION_NAME) ?? "Dev");

 

 

                if (string.IsNullOrWhiteSpace(message.OrderTypeCode))

                {

                    processMessage = OrderRepository.GetOrderPickupDetails(message) as ProcessOrder;

                    if (processMessage == null || string.IsNullOrEmpty(processMessage.OrderTypeCode))

                    {

                        var msg = string.Format("Order with ID {0} was not found in the repository or is invalid.", message.OrderID);

                        _logger.Info(msg);

                        if (processMessage == null)

                            processMessage = message;

                        Bus.Publish(new OrderFailed

                        {

                            BroadcastStatus = true,

                            OrderID = processMessage.OrderID,

                            OrderTypeCode = processMessage.OrderTypeCode,

                            WorkflowTypeCode = processMessage.WorkflowTypeCode,

                            Message = msg

                        });

                        MarkAsComplete();

                        return;

                    }

 

                    processMessage.OrderTypeCode = processMessage.OrderTypeCode.Split(':')[0];

 

                    Data.ProcessOrder = processMessage;

                }

 

                if (processMessage.ExecutionTime > DateTime.UtcNow + config.OrderSettings.ExecutionDelayAllowedTimeOffset || processMessage.OverrideExecutionTime != null || processMessage.OverrideExecutionDelay != null)

                {

                    Data.TimeOutVersion++;

 

                    if (processMessage.OverrideExecutionTime != null)

                    {

                        _logger.InfoFormat("Order {0} delayed execution until {1} because of caller override. Timeout version {2}", processMessage.OrderID, processMessage.OverrideExecutionTime, Data.TimeOutVersion);

                        var overrideExecutionTime = new DateTime(processMessage.OverrideExecutionTime.Value.Ticks, DateTimeKind.Utc);

                        RequestTimeout<ProcessOrderLater>(overrideExecutionTime, new ProcessOrderLater { TimeOutVersion = Data.TimeOutVersion });

                    }

                    if (processMessage.OverrideExecutionDelay != null)

                    {

                        _logger.InfoFormat("Order {0} delayed execution by {1} because of caller override. Timeout version {2}", processMessage.OrderID, processMessage.OverrideExecutionDelay, Data.TimeOutVersion);

                        RequestTimeout<ProcessOrderLater>(processMessage.OverrideExecutionDelay.Value, new ProcessOrderLater { TimeOutVersion = Data.TimeOutVersion });

                   }

                    if (processMessage.ExecutionTime > DateTime.UtcNow + config.OrderSettings.ExecutionDelayAllowedTimeOffset)

                    {

                        _logger.InfoFormat("Order {0} delayed execution until {1} because of EventDateTime. Timeout version {2}", processMessage.OrderID, processMessage.ExecutionTime, Data.TimeOutVersion);

                        var executionTime = processMessage.ExecutionTime;

                        RequestTimeout<ProcessOrderLater>(executionTime, new ProcessOrderLater { TimeOutVersion = Data.TimeOutVersion });

                    }

 

                    return;

                }

 

                var mappedOrder = OrderMapper.MapOrder(processMessage);

                mappedOrder.IgnoreOrderExecutionTimeAndDelay = false;

 

                _logger.DebugFormat("Publishing {0} with order id {1}/order type {2}", mappedOrder.GetType().Name, mappedOrder.OrderID, mappedOrder.OrderTypeCode);

                Bus.Publish(mappedOrder);

 

            }

            catch (Exception ex)

            {

                var msg = string.Format("ProcessOrder failed for order {0} ({1})", processMessage.OrderID, ex.Message);

                _logger.Error(msg, ex);

                if (processMessage == null)

                    processMessage = message;

                Bus.Publish(new OrderFailed

                {

                    BroadcastStatus = processMessage.BroadcastStatus,

                    OrderID = processMessage.OrderID,

                    OrderTypeCode = processMessage.OrderTypeCode,

                    WorkflowTypeCode = processMessage.WorkflowTypeCode,

                    Message = msg

                });

            }

 

            RequestTimeout<ProcessOrderTimeout>(new TimeSpan(7, 0, 0, 0)); // Clean up the saga after a week in case something goes awry

        }

 

        public void Timeout(ProcessOrderLater timeout)

        {

            _logger.DebugFormat("ProcessOrderLater version {0} for order id {1}", timeout.TimeOutVersion, Data.ProcessOrder.OrderID);

            if (timeout.TimeOutVersion != Data.TimeOutVersion)

            {

                _logger.InfoFormat("ProcessOrderLater version {0} for order id {1} is cancelled. Exiting.", timeout.TimeOutVersion, Data.ProcessOrder.OrderID);

                return;

            }

 

            Data.ProcessOrder.OrderTypeCode = null; // force reload of order details

 

           _logger.DebugFormat("Rehandling order {0} after execution timeout", Data.ProcessOrder.OrderID);

            HandleImplementation(Data.ProcessOrder);

        }

 

        public void Timeout(ProcessOrderTimeout timeout)

        {

            _logger.InfoFormat("OrderService timeout  order id {0}. Cleaning up.", Data.ProcessOrder.OrderID);

            MarkAsComplete();

        }

 

        public void Handle(OrderProcessed message)

        {

            _logger.DebugFormat("OrderService received " + message.GetType().Name);

 

            var response = new ProcessOrderResponse

            {

                OrderID = message.OrderID,

                OrderTypeCode = message.OrderTypeCode,

                OrderConfigCode = Data.ProcessOrder.OrderConfigCode,

                OrderHandlerFullyQualifiedName = Data.ProcessOrder.OrderHandlerFullyQualifiedName,

                CorrelationID = message.CorrelationID,

                ResponseCode = ProcessOrderResponseCode.OK.ToString(),

               BroadcastStatus = message.BroadcastStatus

            };

            ReplyToOriginator(response);

 

            //if (!ProcessFollowUpOrders(message.FollowUpOrders))

            ProcessFollowUpOrders(message.FollowUpOrders);

            checkAndMarkAsComplete();

        }

 

 

        private bool ProcessFollowUpOrders(IEnumerable<int> followUpOrders)

        {

            if (followUpOrders == null || followUpOrders.Count() == 0)

            {

                _logger.Debug("No follow up orders.");

                return false;

            }

 

            foreach (var orderID in followUpOrders)

            {

                _logger.DebugFormat("Adding follow up order with id {0}", orderID);

                var order = new ProcessOrder

                {

                    CorrelationID = Data.CorrelationID,

                    OrderID = orderID

                };

                Bus.SendLocal(order);

            }

 

            return true;

        }

 

        private void checkAndMarkAsComplete()

        {

            // TODO: Find a good solution to knowing whether there are more outstanding orders on this saga.

 

            //if (Data.ActiveOrders == null || Data.ActiveOrders.Count == 0)

            //MarkAsComplete();

        }

 

 

    }

 

 

 

 

Morten Petterøe

Chief Technology Officer

________________________________________________________________________________

--
You received this message because you are subscribed to a topic in the Google Groups "Particular Software" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/particularsoftware/FtN-z7GD0PA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to particularsoftw...@googlegroups.com.


To post to this group, send email to particula...@googlegroups.com.
Visit this group at http://groups.google.com/group/particularsoftware.

Szymon Pobiega

unread,
Sep 10, 2015, 7:19:36 AM9/10/15
to particula...@googlegroups.com
Hi Morten

Trying to understand the flow, a ProcessOrderLater timeout seems to be only requested from the method that handles ProcessOrderLater timeout. How can I trigger that timeout initially? Or is this irrelevant to the whole story? Looking at handling of the follow up orders, there is no difference between 0 an any follow up orders other than Bus.SendLocal. Would you be able to capture the saga state updates in both cases using either SQL Server Provider or NHibernate debug log (not sure which logging framework are you using).

Szymon

Morten Petteroe

unread,
Sep 10, 2015, 7:36:48 AM9/10/15
to particula...@googlegroups.com

Hi again,

 

The ProcessOrderLater timeout is requested both in the ProcessOrderLater timeout handler and in the HandleImplementation method itself (look in the “if (processMessage.ExecutionTime…” block).

You are right, there is no other difference between 0 or n follow up orders than Bus.SendLocal. It’s a kind of a recursive process. The order handler keeps calling itself until there are no more orders to process.

I am using log4net using UDP to log2console, so I should be able to give what you want from the Nhibernate log. Do you want everything, or just some sections of the debug log?

 

 

Thanks,

 

Morten

Szymon Pobiega

unread,
Sep 10, 2015, 8:11:25 AM9/10/15
to particula...@googlegroups.com
Hi Morten

The thing is, HandleImplementation method seems like it is only used from the public void Timeout(ProcessOrderLater timeout) method. That's why I can't see how this can ever be requested unless HandleImplementation is called somehow from outside (which seems impossible since it is a private method). 

Regarding logs, I am interested in the actual SQL statements produced by NHibernate.

Szymon

Morten Petteroe

unread,
Sep 10, 2015, 8:44:29 AM9/10/15
to particula...@googlegroups.com

Ah. I see the confusion J

 

This project started as a ServiceMatrix solution. The actual handler implementation is in another partial class. So “HandleImplementation” in my class is the same as a regular Handle(ProcessOrder message) implementation. That means that the HandleImplementation is called on every incoming ProcessOrder command. Here is the actual handler code:

 

              public void Handle(ProcessOrder message)

              {

                     // Store message in Saga Data for later use

                     this.Data.ProcessOrder = message;

                     // Handle message on partial class

                     this.HandleImplementation(message);

 

                     // Check if Saga is Completed

                     CheckIfAllMessagesReceived();

              }

 

 

I have run a new order and put SQL Profiler on it to get the SQL log. I have attached the trace file here. The SQL trace shows the flow below. As you can see in the sequence diagram from ServiceInsight, the first order processes fine, but the follow up order fails.

 

 

NHibernateTrace.trc

Szymon Pobiega

unread,
Sep 10, 2015, 9:20:33 AM9/10/15
to particula...@googlegroups.com
Hi Morten

Looking at the SQL trace I can see that there is a number of SLR attempts and they all fail. There is no 'rouge' update that would cause the optimistic concurrency failure. Also, the selects from saga data table use the upgrade lock so nobody else should be able to modify them. That might mean that the problem is with mismatch between the schema and the data. Can you send me the table creation script exported from the SQL server? On your end, you can also verify if all the values used by `exec sp_executesql N'UPDATE ProcessOrderHandlerSagaData ...` statements you attached in the trace match the columns they are meant to update.

The precision of some values might be lost either when NH loads them from the DB or when it prepares the UPDATE statement. The cause of this loss might be the mismatch between the mappings NSB auto-generates and the schema of the database. That should not happen if the schema is auto-generated by NServiceBus but there is always a possibility of a bug (either in NServiceBus or NHibernate).

Szymon

Morten Petteroe

unread,
Sep 10, 2015, 9:31:54 AM9/10/15
to particula...@googlegroups.com

Hi again Szymon,

 

I have attached the table scripts. I will go through the update statements in the trace to see if I see anything wrong.

 

If the problem is with precisions, how do I fix this? Is there a way to tell Nhibernate how my data structure looks?

tablescripts.txt

Morten Petteroe

unread,
Sep 10, 2015, 10:42:05 AM9/10/15
to particula...@googlegroups.com

Hi again Szymon,

 

I think I am a bit closer to seeing what the problem is.

 

My saga has Saga Data properties containing various incoming messages that are used during the Saga lifetime. I am correlating my sagas based on a Guid property “CorrelationID” on every order.

 

When NHibernate does the initial Saga “INSERT”, it correctly inserts NULL on the CorrelationID on the empty message properties. For example, I have a property on my ProcessOrderHandlerSagaData called GenerateSmsOrderProcessed, which is a message that is handled by the Saga. When the saga is first invoked, this property is null. The following is a snippet of the NHibernate “INSERT” which correctly sets the CorrelationID of the empty GenerateSmsOrderProcessed property to null:

 

GenerateSmsOrderProcessedCorrelationID

@p36,                                

@p36 uniqueidentifier,               

@p36=NULL,                           

 

When the next saga invocation is made, the GenerateSmsOrderProcessed property is still empty, but the “SELECT” made by NHibernate is wrong. It assumes a Guid.Empty instead of a null value on the missing CorrelationID. Which means that the SELECT returns zero rows. Here is a snippet from the NHibernate “SELECT”:

 

AND GenerateSmsOrderProcessedCorrelationID = @p41

     @p41 uniqueidentifier,                      

     @p41='00000000-0000-0000-0000-000000000000',

 

Is there a way to trick NHibernate to select only based on the “CorrelationID” property on the Order message (the property I use to map messages to Sagas)? The long list of “AND” statements in the select seems to be a very brittle piece of functionality.

Morten Petteroe

unread,
Sep 10, 2015, 10:59:07 AM9/10/15
to particula...@googlegroups.com

Sorry; when I write “SELECT” below, I mean “UPDATE”….

 

 

 

Morten Petterøe

Chief Technology Officer

________________________________________________________________________________

 

Szymon Pobiega

unread,
Sep 11, 2015, 3:31:34 AM9/11/15
to particula...@googlegroups.com
Hi Morten

Yeah, that queries looks odd. Can you post your saga data class? I've tried to repro it with a single Guid field but failed:
 * The database column was created as nullable, as in your script
 * When the field was not nullable, the INSERT contained Guid.Empty value (not NULL like in you trace) and the UPDATE also has WHERE with Guid.Empty
 * When the field was nullable (Guid?), the INSERT contained NULL and the UPDATE also has WHERE with "is null" instead of Guid.Empty as in your trace

That said, you can use an explicit version field to get rid of the WHERE in the updates: http://docs.particular.net/nservicebus/nhibernate/saga-concurrency#explicit-version

Cheers,
Szymon

Morten Petteroe

unread,
Sep 11, 2015, 5:31:17 AM9/11/15
to particula...@googlegroups.com

Thanks Szymon,

 

I will try the RowVersion trick you mention here. In the mean time, here is my saga data class

 

    public partial class ProcessOrderHandlerSagaData

    {

           public virtual Guid Id { get; set; }

           public virtual string Originator { get; set; }

           public virtual string OriginalMessageId { get; set; }

 

           public virtual ProcessOrder ProcessOrder { get; set; }

           public virtual EmailOrderProcessed GenerateEmailOrderProcessed { get; set; }

           public virtual SendEmailOrderProcessed SendEmailOrderProcessed { get; set; }

           public virtual GenerateSmsOrderProcessed GenerateSmsOrderProcessed { get; set; }

          public virtual SendSmsOrderProcessed SendSmsOrderProcessed { get; set; }

 

              [Unique]

              public virtual Guid CorrelationID

              {

                     get

                     {

                           if (ProcessOrder == null)

                                  return Guid.Empty;

                           else

                                  return ProcessOrder.CorrelationID;

                     }

              }

 

              public virtual int TimeOutVersion { get; set; }

Szymon Pobiega

unread,
Sep 11, 2015, 5:34:49 AM9/11/15
to particula...@googlegroups.com
Hi

OK. Can you also attach GenerateSmsOrderProcessed 

Szymon

Morten Petteroe

unread,
Sep 11, 2015, 5:35:05 AM9/11/15
to particula...@googlegroups.com

Btw;

 

All the XxxOrderProcessed messages derive from this one:

 

       public class OrderProcessed : IOrderProcessed, ISagaCorrelator

       {

              public int OrderID { get; set; }

 

              public IEnumerable<int> FollowUpOrders { get; set; }

 

              public string OrderTypeCode { get; set; }

 

              public string WorkflowTypeCode { get; set; }

 

              public bool BroadcastStatus { get; set; }

 

              public string Operator { get; set; }

 

              public Guid CorrelationID { get; set; }

       }

 

 

 

 

Morten Petterøe

Chief Technology Officer

________________________________________________________________________________

 

Morten Petteroe

unread,
Sep 11, 2015, 5:36:45 AM9/11/15
to particula...@googlegroups.com

Here it is. See OrderProcessed in my previous email

 

    public class GenerateSmsOrderProcessed : OrderProcessed

    {

Morten Petteroe

unread,
Sep 11, 2015, 5:41:06 AM9/11/15
to particula...@googlegroups.com

One quick question;

 

Will these problems go away if I switch to RavenDB?

Morten Petteroe

unread,
Sep 11, 2015, 6:21:42 AM9/11/15
to particula...@googlegroups.com

Hi again,

 

I have now done one test with the [RowVersion] attribute, and so far it looks promising J. This was just one test, so I need to use the system a little bit to see it it holds up with load.

 

 

 

 

Morten Petterøe

Chief Technology Officer

________________________________________________________________________________

 

Szymon Pobiega

unread,
Sep 11, 2015, 6:48:27 AM9/11/15
to particula...@googlegroups.com
Hi

I did some tests with a simplified scenario where my saga data class contains a nested object that has a collection of ints (similar to you FollowUpOrders) and got similar error. Apparently NHibernate generates different SQLs depending on the presence of that collection. Enabling explicit version numbers only hidden/deferred the problem instead of solving it.

First, we generally do not encourage storing whole messages as part of the saga. Saga data should contain minimum information required to execute the business logic, ideally it should not contain nested object. 
Second, NHibernate persistence by default only supports nested collections of objects, assuming these objects contain a Guid Id property. It does not support nested components. In order to support such structure, you would have to prepare the mapping yourself as described here: http://docs.particular.net/samples/nhibernate/custom-mappings/
Third, in RavenDB the core should work without any problems since RavenDB just serializes whole object graph to JSON. The downside is, RavenDB saga persister is less performance than the NHibernate one, especially in high-contention scenarios.

I'd recommend going with option number 1: putting the saga data on a diet. If, after that treatment, the saga data still cannot be mapped using the default conventions, I'd recommend applying number 2. 

Szymon


Morten Petteroe

unread,
Sep 11, 2015, 7:18:56 AM9/11/15
to particula...@googlegroups.com

Thank you Szymon for spending time on this problem.

 

I will put the saga data on a diet J. It was the ServiceMatrix generator that made this code, so I didn’t think much about it. But there is certainly room for losing a bit of extra fat here.

cid:image001.png@01D0EC90.9185B890

Reply all
Reply to author
Forward
0 new messages