我在一个长期运行过程的各种状态下都有一堆现有的传奇。
最近,我们决定使用Saga.UniqueAttribute
(此处更多http://docs.particular.net/nservicebus/nservicebus-sagas-and-concurrency)使我们的IContainSagaData实施中的某个属性与众不同。
在部署更改之后,我们意识到我们所有旧的saga实例都没有找到,并且在进一步digging之后(感谢Charlie!)发现通过添加unique属性,我们需要数据修复我们所有的乌鸦现有的传奇。
现在,这非常糟糕,有点像向数据库列添加索引,然后发现所有表数据不再可选,但实际上是这样,我们决定创建一个用于执行此操作的工具。
所以在创建和运行这个工具之后,我们现在修补了旧的传奇,这样他们现在就像新的传奇(自从我们改变了以来创建的传奇)。
然而,尽管所有数据现在看起来都正确,但我们仍然无法找到传奇的旧实例!
我们编写的工具做了两件事。对于每个现有的传奇,工具:
我的问题是:
添加元数据令牌的代码:
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程序集。
答案 0 :(得分:3)
简短的回答是,使数据与新的身份证件相似是不够的。在使用Guid.NewGuid().ToString()
的地方,这些数据非常重要!这就是为什么您的解决方案现在无法正常工作的原因。我在2014年RavenConf的演讲的最后一个季度 - here are the slides and video中谈到了身份证件的概念(特别是关于NServiceBus用例)。
所以这是一个很长的答案:
在RavenDB中,唯一的ACID保证在Id操作的加载/存储上。因此,如果两个线程同时在同一个Saga上运行,并且一个存储Saga数据,则第二个线程只能获得正确的传奇数据,如果它还通过其Id加载文档。
为了保证这一点,Raven Saga Persister使用身份证明文件,就像您展示的那样。它包含SagaId
,UniqueValue
(主要用于人工理解和调试,数据库在技术上并不需要它),以及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来做到这一点。