将[Unique]属性应用于IContainSagaData属性后的现有saga实例

时间:2014-05-21 14:12:56

标签: nservicebus nservicebus-sagas

我在一个长期运行过程的各种状态下都有一堆现有的传奇。

最近,我们决定使用Saga.UniqueAttribute(此处更多http://docs.particular.net/nservicebus/nservicebus-sagas-and-concurrency)使我们的IContainSagaData实施中的某个属性与众不同。

在部署更改之后,我们意识到我们所有旧的saga实例都没有找到,并且在进一步digging之后(感谢Charlie!)发现通过添加unique属性,我们需要数据修复我们所有的乌鸦现有的传奇。

现在,这非常糟糕,有点像向数据库列添加索引,然后发现所有表数据不再可选,但实际上是这样,我们决定创建一个用于执行此操作的工具。

所以在创建和运行这个工具之后,我们现在修补了旧的传奇,这样他们现在就像新的传奇(自从我们改变了以来创建的传奇)。

然而,尽管所有数据现在看起来都正确,但我们仍然无法找到传奇的旧实例!

我们编写的工具做了两件事。对于每个现有的传奇,工具:

  1. 添加一个名为" NServiceBus-UniqueValue"的新RavenJToken。对于saga元数据,将值设置为与该saga的唯一属性相同的值,并
  2. 创建NServiceBus.Persistence.Raven.SagaPersister.SagaUniqueIdentity类型的新文档,相应地设置SagaId,SagaDocId和UniqueValue字段。
  3. 我的问题是:

    1. 仅仅使数据看起来正确还是我们需要做的其他事情就足够了?
    2. 我们的另一个选择是还原添加了唯一属性的更改。但是,在这种情况下,自改变以来创建的那些新传奇是否可以用这个?
    3. 添加元数据令牌的代码:

      var policyKey = RavenJToken.FromObject(saga.PolicyKey); // This is the unique field
      sagaDataMetadata.Add("NServiceBus-UniqueValue", policyKey);
      

      添加新文档的代码:

      var policyKeySagaUniqueId = new SagaUniqueIdentity
                                  {
                                      Id = "Matlock.Renewals.RenewalSaga.RenewalSagaData/PolicyKey/" + Guid.NewGuid().ToString(), 
                                      SagaId = saga.Id,
                                      UniqueValue = saga.PolicyKey,
                                      SagaDocId = "RenewalSaga/" + saga.Id.ToString()
                                  };
      
       session.Store(policyKeySagaUniqueId);
      

      任何帮助都非常感激。

      修改

      感谢David的帮助,我们已经解决了问题 - 主要区别在于我们使用SagaUniqueIdentity.FormatId()来生成我们的文档ID而不是新的guid - 这是微不足道的事情,因为我们是已经引用了NServiceBus和NServiceBus.Core程序集。

1 个答案:

答案 0 :(得分:3)

简短的回答是,使数据与新的身份证件相似是不够的。在使用Guid.NewGuid().ToString()的地方,这些数据非常重要!这就是为什么您的解决方案现在无法正常工作的原因。我在2014年RavenConf的演讲的最后一个季度 - here are the slides and video中谈到了身份证件的概念(特别是关于NServiceBus用例)。

所以这是一个很长的答案:

在RavenDB中,唯一的ACID保证在Id操作的加载/存储上。因此,如果两个线程同时在同一个Saga上运行,并且一个存储Saga数据,则第二个线程只能获得正确的传奇数据,如果它还通过其Id加载文档。

为了保证这一点,Raven Saga Persister使用身份证明文件,就像您展示的那样。它包含SagaIdUniqueValue(主要用于人工理解和调试,数据库在技术上并不需要它),以及SagaDocId(这是一个小重复,因为它只有我们已经拥有{SagaTypeName}/{SagaId}的{​​{1}}。

使用SagaDocId,我们可以使用RavenDB的Include功能来执行这样的查询(来自内存,可能是错误的,并且应该仅用于将概念说明为伪代码)...

SagaId

那么var identityDocId = // some value based on incoming message var idDoc = RavenSession // Look at the identity doc's SagaDocId and pull back that document too! .Include<SagaIdentity>(identityDoc => identityDoc.SagaDocId) .Load(identityDocId); var sagaData = RavenSession .Load(idDoc.SagaDocId); // Already in-memory, no 2nd round-trip to database! 非常重要,因为它描述了来自消息的价值的唯一性,而不仅仅是任何旧的Guid会做的。所以我们真正需要知道的是如何计算它。

为此,NServiceBus saga persister code具有指导意义:

identityDocId

重要的部分是来自同一文件的SagaUniqueIdentity.FormatId方法。

void StoreUniqueProperty(IContainSagaData saga)
{
    var uniqueProperty = UniqueAttribute.GetUniqueProperty(saga);

    if (!uniqueProperty.HasValue) return;

    var id = SagaUniqueIdentity.FormatId(saga.GetType(), uniqueProperty.Value);
    var sagaDocId = sessionFactory.Store.Conventions.FindFullDocumentKeyFromNonStringIdentifier(saga.Id, saga.GetType(), false);

    Session.Store(new SagaUniqueIdentity
    {
        Id = id, 
        SagaId = saga.Id, 
        UniqueValue = uniqueProperty.Value.Value,
        SagaDocId = sagaDocId
    });

    SetUniqueValueMetadata(saga, uniqueProperty.Value);
}

这依赖于Utils.DeterministicGuid.Create(params object[] data),它从MD5哈希中创建了一个Guid。 (MD5实际安全性很差,但我们只是寻找可能的唯一性。)

public static string FormatId(Type sagaType, KeyValuePair<string, object> uniqueProperty)
{
    if (uniqueProperty.Value == null)
    {
        throw new ArgumentNullException("uniqueProperty", string.Format("Property {0} is marked with the [Unique] attribute on {1} but contains a null value. Please make sure that all unique properties are set on your SagaData and/or that you have marked the correct properties as unique.", uniqueProperty.Key, sagaType.Name));
    }

    var value = Utils.DeterministicGuid.Create(uniqueProperty.Value.ToString());

    var id = string.Format("{0}/{1}/{2}", sagaType.FullName.Replace('+', '-'), uniqueProperty.Key, value);

    // raven has a size limit of 255 bytes == 127 unicode chars
    if (id.Length > 127)
    {
        // generate a guid from the hash:
        var key = Utils.DeterministicGuid.Create(sagaType.FullName, uniqueProperty.Key);

        id = string.Format("MoreThan127/{0}/{1}", key, value);
    }

    return id;
}

您需要复制的内容才能使您的实用程序正常运行。

真正有趣的是,这段代码让它一直到制作 - 我很惊讶你在此之前没有遇到麻烦,消息创建了新的传奇实例,当他们真的不应该这样做时#39;因为他们无法找到现有的Saga数据。

我几乎认为,如果NServiceBus在您尝试通过[Unique]标记属性以外的任何内容找到Saga Data时会发出警告,这可能是一个好主意,因为忘记这样做很容易。我在GitHub上提交了this issue并提交this pull request来做到这一点。