1

我有一堆处于长期运行过程的各种状态的现有 sagas。

Saga.UniqueAttribute最近,我们决定通过使用(更多关于这里http://docs.particular.net/nservicebus/nservicebus-sagas-and-concurrency)来使我们的 IContainSagaData 实现上的属性之一独一无二。

部署更改后,我们意识到没有找到所有旧的 saga 实例,并且在进一步挖掘(感谢 Charlie!)后发现,通过添加唯一属性,我们需要对 Raven 中的所有现有 saga 进行数据修复。

现在,这很糟糕,有点像在数据库列中添加索引,然后发现所有表数据不再可选择,但既然如此,我们决定创建一个工具来执行此操作。

因此,在创建并运行此工具之后,我们现在已经修补了旧的 sagas,使它们现在类似于新的 sagas(自从我们接受更改后创建的 sagas)。

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

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

  1. 将一个名为“NServiceBus-UniqueValue”的新 RavenJToken 添加到 saga 元数据中,将值设置为与该 saga 的唯一属性相同的值,并且
  2. 创建一个 NServiceBus.Persistence.Raven.SagaPersister.SagaUniqueIdentity 类型的新文档,相应地设置 SagaId、SagaDocId 和 UniqueValue 字段。

我的问题是:

  1. 仅仅使数据看起来正确就足够了,还是我们需要做其他事情?
  2. 我们的另一个选择是还原添加了唯一属性的更改。但是,在这种情况下,自从更改发生以来创建的那些新 sagas 是否可以接受?

添加元数据令牌的代码:

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 程序集。

4

1 回答 1

3

简短的回答是,仅仅使数据类似于新的身份证件是不够的。您在哪里使用Guid.NewGuid().ToString(),该数据很重要!这就是为什么您的解决方案现在不起作用的原因。我在 RavenConf 2014 演讲的最后一个季度谈到了身份证件的概念(特别是关于 NServiceBus 用例)——这里是幻灯片和视频

所以这里是长答案:

在 RavenDB 中,唯一的 ACID 保证是通过 Id 操作加载/存储。因此,如果两个线程同时对同一个 Saga 进行操作,并且一个线程存储 Saga 数据,那么第二个线程只有在通过其 Id 加载文档时才能期望返回正确的 saga 数据。

为了保证这一点,Raven Saga Persister 使用了与您展示的一样的身份证件。它包含SagaId、 (主要用于人类理解和调试,数据库在UniqueValue技术上不需要它SagaDocId)和{SagaTypeName}/{SagaId}SagaId

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

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!

因此,identityDocId非常重要,因为它描述了来自消息的值的唯一性,而不仅仅是任何旧的 Guid 都会这样做。所以我们真正需要知道的是如何计算它。

为此,NServiceBus 传奇持久化代码具有指导意义:

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);
}

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

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;
}

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

static class DeterministicGuid
{
    public static Guid Create(params object[] data)
    {
        // use MD5 hash to get a 16-byte hash of the string
        using (var provider = new MD5CryptoServiceProvider())
        {
            var inputBytes = Encoding.Default.GetBytes(String.Concat(data));
            var hashBytes = provider.ComputeHash(inputBytes);
            // generate a guid from the hash:
            return new Guid(hashBytes);
        }
    } 
}

这就是您需要复制以使您的实用程序正常工作的内容。

真正有趣的是,这段代码一直在生产——我很惊讶你在此之前没有遇到麻烦,消息创建了新的 saga 实例,而实际上它们不应该因为找不到现有的 Saga数据。

我几乎认为,如果 NServiceBus 会在您尝试通过 [Unique] 标记属性以外的任何其他方式查找 Saga Data 时发出警告,这可能是一个好主意,因为这很容易忘记做。我在 GitHub 上提交了这个问题并提交了这个拉取请求来做到这一点。

于 2014-05-21T18:12:33.760 回答