快照和预测
#aws #测试 #go #eventdriven

在GO中建立DynamoDB事件商店:快照和投影

草稿

Part 2中,我们为活动商店构建了AppendQuery功能,这使我们列出了此事件列表:

store.Event{Id:"58f02691-78ed-4ca5-8e59-8f4deb44e063", Version:0, CharacterName:"cpustejovsky", CharacterHitPoints:8, Note:"Init"}
store.Event{Id:"58f02691-78ed-4ca5-8e59-8f4deb44e063", Version:1, CharacterName:"cpustejovsky", CharacterHitPoints:-2, Note:"Slashing damage from goblin"}
store.Event{Id:"58f02691-78ed-4ca5-8e59-8f4deb44e063", Version:2, CharacterName:"cpustejovsky", CharacterHitPoints:-3, Note:"bludgeoning damage from bugbear"}

要利用从查询中返回的一长串事件并管理越来越长的事件列表,我们将构建为ProjectSnapshot我们的事件构建功能。

投影

我们可以在列表中的每个事件中手动将CharacterHitPoints添加在一起,并将它们添加在一起以获取我们角色的当前生命值。 (8 + -2 + -3 = 3)。

为了使其易于使用,我们可以创建一个投影。投影将采取我们拥有的事件流,并将其读取。在这种情况下,您可以想象投影到UI的值以显示播放器角色的当前生命值。 Project将占用Event ID,并以重构状态返回Event。让我们首先写我们的测试:

t.Run("Project Events from Event Store", func(t *testing.T) {
    aggEvent, err := es.Project(ctx, id)
    require.Nil(t, err)
    assert.Equal(t, 8, aggEvent.CharacterHitPoints)
    assert.Equal(t, "cpustejovsky", aggEvent.CharacterName)
})

通过测试失败,我们可以开始使其通过。我们需要查询我们的活动商店并进行范围遍历活动,将它们重新安置到一个状态。

// Project takes an id, queries events since the last snapshot, and returns a reconstituted Event
func (es *EventStore) Project(ctx context.Context, id string) (*Event, error) {
    var agg Event
    //Query events
    events, err := es.QueryAll(ctx, id)
    if err != nil {
        return nil, err
    }
    //Reconstitute the event
    for i, event := range events {
        if i == len(events)-1 {
            agg.Id = event.Id
            agg.CharacterName = event.CharacterName
            agg.Version = event.Version + 1
        }
        agg.CharacterHitPoints += event.CharacterHitPoints
    }
    return &agg, nil
}

现在我们的测试应该通过。作为重构,我们可以将重构逻辑从Project方法分开:

// Project takes an id, queries events since the last snapshot, and returns an aggregated Event
func (es *EventStore) Project(ctx context.Context, id string) (*Event, error) {
    events, err := es.QueryAll(ctx, id)
    if err != nil {
        return nil, err
    }
    agg := reconstituteEvent(events)
    return &agg, nil
}

func reconstituteEvent(events []Event) Event {
    var agg Event
    for i, event := range events {
        if i == len(events)-1 {
            agg.Id = event.Id
            agg.CharacterName = event.CharacterName
            agg.Version = event.Version + 1
        }
        agg.CharacterHitPoints += event.CharacterHitPoints
    }
    return agg
}

快照

由于我们有更多的DND会话,并且对玩家角色的生命值进行了越来越多的更改,因此我们将遇到麻烦处理日益增长的事件。为了帮助这一点,我们可以在活动日志中的点上拍摄状态的快照。这将意味着活动商店每次投影时都不需要查询每个事件。

首先,让我们写我们的测试。我们的Snapshot方法将采用上下文和重构事件e并返回错误:

t.Run("Snapshot should return no error", func(t *testing.T) {  
   e, err := es.Project(ctx, id)  
   assert.Nil(t, err)  
   err = es.Snapshot(ctx, e)  
   assert.Nil(t, err)  
})

有多种方法可以实现此功能,但是目前,我坚持使用DynamoDB的单个表设计,并使用同一表作为快照与标准事件使用。结果,我们的Snapshot方法是使用Append方法将快照事件添加到:

// using a constant instead of a hardcoded value for it to be easily used in multiple places 
const SnapshotValue string = "SNAPSHOT"
//...
func (es *EventStore) Snapshot(ctx context.Context, agg *Event) error {  
   e := &Event{  
      Id:                 agg.Id,  
      Version:            agg.Version,  
      CharacterName:      agg.CharacterName,  
      CharacterHitPoints: agg.CharacterHitPoints,  
      Note:               SnapshotValue,  
   }  
   return es.Append(ctx, e)  
}

Snapshot测试通过时,我们的下一步应该是查看我们的QueryAll是否仍然有效。让我们写测试:

t.Run("QueryAll should not return the snapshot", func(t *testing.T) {  
   queriedEvents, err := es.QueryAll(ctx, id)  
   assert.Nil(t, err)  
   assert.Equal(t, len(events), len(queriedEvents))  
   for _, event := range events {  
      assert.NotEqual(t, store.SnapshotValue, event.Note)  
   }  
})

此测试失败了,因为我们没有修改QueryAll来跳过快照。我们可以对DynamoDB参数进行修改以使测试工作:

func (es *EventStore) QueryAll(ctx context.Context, id string) ([]Event, error) {
params := dynamodb.QueryInput{  
   TableName:              aws.String(es.Table),  
   KeyConditionExpression: aws.String("Id = :uuid"),  
   FilterExpression:       aws.String("Note <> :note"),  
   ExpressionAttributeValues: map[string]types.AttributeValue{  
      ":uuid": &types.AttributeValueMemberS{Value: id},  
      ":note": &types.AttributeValueMemberS{Value: SnapshotValue},
   },  
}
//...

评估

您可以从第3部分here查看我们当前的代码。有很多功能可以添加,但是我想在接下来的两个部分中解决两个关键问题。

首先,活动商店只能跟踪玩家角色的生命值。我们需要抽象Event才能代表无数的不同事件。

第二,我们将活动存储列为DynamoDB客户端。这对于我们的集成测试很好,但会为任何依赖我们的活动商店的代码进行负担,但不关注活动商店的行为。为了解决这个问题,我们将使用接口和模拟。

有任何问题或评论吗?关于事件预测或快照,我错过或错了吗?让我在评论中知道或在TwitterGopher's Slack上与我联系。