Skip to content

Commit 098c00c

Browse files
committed
modify GetReplicationMessages to support prioritization of responses by oldest creationTime of replication messages
1 parent 88fd67d commit 098c00c

File tree

2 files changed

+200
-26
lines changed

2 files changed

+200
-26
lines changed

service/history/handler/handler.go

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"context"
2525
"errors"
2626
"fmt"
27+
"slices"
2728
"sync"
2829
"sync/atomic"
2930
"time"
@@ -1558,20 +1559,34 @@ func (h *handlerImpl) GetReplicationMessages(
15581559
return nil, constants.ErrShuttingDown
15591560
}
15601561

1562+
msgs := h.getReplicationShardMessages(ctx, request)
1563+
response := h.buildGetReplicationMessagesResponse(metricsScope, msgs)
1564+
1565+
h.GetLogger().Debug("GetReplicationMessages succeeded.")
1566+
return response, nil
1567+
}
1568+
1569+
// getReplicationShardMessages gets replication messages from all the shards of the request
1570+
// it queries the replication tasks from each shard in parallel
1571+
// and returns the replication tasks in the order of the request tokens
1572+
func (h *handlerImpl) getReplicationShardMessages(
1573+
ctx context.Context,
1574+
request *types.GetReplicationMessagesRequest,
1575+
) []replicationShardMessages {
15611576
var wg sync.WaitGroup
1562-
wg.Add(len(request.Tokens))
1563-
result := new(sync.Map)
1577+
var results = make([]replicationShardMessages, len(request.Tokens))
15641578

1565-
for _, token := range request.Tokens {
1566-
go func(token *types.ReplicationToken) {
1579+
wg.Add(len(request.Tokens))
1580+
for i, token := range request.Tokens {
1581+
go func(i int, token *types.ReplicationToken) {
15671582
defer wg.Done()
15681583

15691584
engine, err := h.controller.GetEngineForShard(int(token.GetShardID()))
15701585
if err != nil {
15711586
h.GetLogger().Warn("History engine not found for shard", tag.Error(err))
15721587
return
15731588
}
1574-
tasks, err := engine.GetReplicationMessages(
1589+
msgs, err := engine.GetReplicationMessages(
15751590
ctx,
15761591
request.GetClusterName(),
15771592
token.GetLastRetrievedMessageID(),
@@ -1581,42 +1596,109 @@ func (h *handlerImpl) GetReplicationMessages(
15811596
return
15821597
}
15831598

1584-
result.Store(token.GetShardID(), tasks)
1585-
}(token)
1599+
results[i] = replicationShardMessages{
1600+
ReplicationMessages: msgs,
1601+
shardID: token.GetShardID(),
1602+
size: proto.FromReplicationMessages(msgs).Size(),
1603+
earliestCreationTime: msgs.GetEarliestCreationTime(),
1604+
}
1605+
}(i, token)
15861606
}
15871607

15881608
wg.Wait()
1609+
return results
1610+
}
15891611

1590-
responseSize := 0
1591-
maxResponseSize := h.config.MaxResponseSize
1592-
1593-
messagesByShard := make(map[int32]*types.ReplicationMessages)
1594-
result.Range(func(key, value interface{}) bool {
1595-
shardID := key.(int32)
1596-
tasks := value.(*types.ReplicationMessages)
1612+
// buildGetReplicationMessagesResponse builds a new GetReplicationMessagesResponse from shard results
1613+
// The response can be partial if the total size of the response exceeds the max size.
1614+
// In this case, responses with oldest replication tasks will be returned
1615+
func (h *handlerImpl) buildGetReplicationMessagesResponse(metricsScope metrics.Scope, msgs []replicationShardMessages) *types.GetReplicationMessagesResponse {
1616+
// Shards with large maessages can cause the response to exceed the max size.
1617+
// In this case, we need to skip some shard messages to make sure the result response size is within the limit.
1618+
// To prevent a replication lag in the future, we should return the messages with the oldest replication task.
1619+
// So we sort the shard messages by the earliest creation time of the replication task.
1620+
// If the earliest creation time is the same, we compare the size of the message.
1621+
// This will sure that shards with the oldest replication tasks will be processed first.
1622+
sortReplicationShardMessages(msgs)
1623+
1624+
var (
1625+
responseSize = 0
1626+
maxResponseSize = h.config.MaxResponseSize
1627+
messagesByShard = make(map[int32]*types.ReplicationMessages, len(msgs))
1628+
)
15971629

1598-
size := proto.FromReplicationMessages(tasks).Size()
1599-
if (responseSize + size) >= maxResponseSize {
1600-
metricsScope.Tagged(metrics.ShardIDTag(int(shardID))).IncCounter(metrics.ReplicationMessageTooLargePerShard)
1630+
for _, m := range msgs {
1631+
if (responseSize + m.size) >= maxResponseSize {
1632+
metricsScope.Tagged(metrics.ShardIDTag(int(m.shardID))).IncCounter(metrics.ReplicationMessageTooLargePerShard)
16011633

16021634
// Log shards that did not fit for debugging purposes
16031635
h.GetLogger().Warn("Replication messages did not fit in the response (history host)",
1604-
tag.ShardID(int(shardID)),
1605-
tag.ResponseSize(size),
1636+
tag.ShardID(int(m.shardID)),
1637+
tag.ResponseSize(m.size),
16061638
tag.ResponseTotalSize(responseSize),
16071639
tag.ResponseMaxSize(maxResponseSize),
16081640
)
1609-
} else {
1610-
responseSize += size
1611-
messagesByShard[shardID] = tasks
1641+
1642+
continue
16121643
}
1644+
responseSize += m.size
1645+
messagesByShard[m.shardID] = m.ReplicationMessages
1646+
}
1647+
return &types.GetReplicationMessagesResponse{MessagesByShard: messagesByShard}
1648+
}
16131649

1614-
return true
1615-
})
1650+
// replicationShardMessages wraps types.ReplicationMessages
1651+
// and contains some metadata of the ReplicationMessages
1652+
type replicationShardMessages struct {
1653+
*types.ReplicationMessages
1654+
// shardID of the ReplicationMessages
1655+
shardID int32
1656+
// size of proto payload of ReplicationMessages
1657+
size int
1658+
// earliestCreationTime of ReplicationMessages
1659+
earliestCreationTime *int64
1660+
}
16161661

1617-
h.GetLogger().Debug("GetReplicationMessages succeeded.")
1662+
// sortReplicationShardMessages sorts the peer responses by the earliest creation time of the replication tasks
1663+
func sortReplicationShardMessages(msgs []replicationShardMessages) {
1664+
slices.SortStableFunc(msgs, cmpReplicationShardMessages)
1665+
}
1666+
1667+
// cmpReplicationShardMessages compares
1668+
// two replicationShardMessages objects by earliest creation time
1669+
// it can be used as a comparison func for slices.SortStableFunc
1670+
// if a's or b's earliestCreationTime is nil, slices.SortStableFunc will put them to the end of a slice
1671+
// otherwise it will compare the earliestCreationTime of the replication tasks
1672+
// if earliestCreationTime is equal, it will compare the size of the response
1673+
func cmpReplicationShardMessages(a, b replicationShardMessages) int {
1674+
// a > b
1675+
if a.earliestCreationTime == nil {
1676+
return 1
1677+
}
1678+
// a < b
1679+
if b.earliestCreationTime == nil {
1680+
return -1
1681+
}
1682+
1683+
// if both are not nil, compare the creation time
1684+
if *a.earliestCreationTime < *b.earliestCreationTime {
1685+
return -1
1686+
}
1687+
1688+
if *a.earliestCreationTime > *b.earliestCreationTime {
1689+
return 1
1690+
}
1691+
1692+
// if both equal, compare the size
1693+
if a.size < b.size {
1694+
return -1
1695+
}
1696+
1697+
if a.size > b.size {
1698+
return 1
1699+
}
16181700

1619-
return &types.GetReplicationMessagesResponse{MessagesByShard: messagesByShard}, nil
1701+
return 0
16201702
}
16211703

16221704
// GetDLQReplicationMessages is called by remote peers to get replicated messages for DLQ merging

service/history/handler/handler_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3655,3 +3655,95 @@ func TestRatelimitUpdate(t *testing.T) {
36553655
"unexpected weights returned from aggregator or serialization. if values differ in a reasonable way, possibly aggregator behavior changed?",
36563656
)
36573657
}
3658+
3659+
func Test_cmpReplicationShardMessages(t *testing.T) {
3660+
for name, c := range map[string]struct {
3661+
a, b replicationShardMessages
3662+
want int
3663+
}{
3664+
"a time is nil, b is empty": {
3665+
a: replicationShardMessages{earliestCreationTime: nil}, want: 1,
3666+
},
3667+
"a time is not nil, b is empty": {
3668+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)}, want: -1,
3669+
},
3670+
"a time is not nil, b time is nil": {
3671+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)},
3672+
b: replicationShardMessages{earliestCreationTime: nil},
3673+
want: -1,
3674+
},
3675+
"a time less b time": {
3676+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)},
3677+
b: replicationShardMessages{earliestCreationTime: common.Int64Ptr(20)},
3678+
want: -1,
3679+
},
3680+
"a time greater b time": {
3681+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(20)},
3682+
b: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)},
3683+
want: 1,
3684+
},
3685+
"a size less b size": {
3686+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10), size: 10},
3687+
b: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10), size: 20},
3688+
want: -1,
3689+
},
3690+
"a size greater b size": {
3691+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10), size: 20},
3692+
b: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10), size: 10},
3693+
want: 1,
3694+
},
3695+
"a equal b": {
3696+
a: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)},
3697+
b: replicationShardMessages{earliestCreationTime: common.Int64Ptr(10)},
3698+
want: 0,
3699+
},
3700+
} {
3701+
t.Run(name, func(t *testing.T) {
3702+
assert.Equal(t, c.want, cmpReplicationShardMessages(c.a, c.b))
3703+
})
3704+
}
3705+
}
3706+
3707+
func Test_sortReplicationShardMessages(t *testing.T) {
3708+
for name, c := range map[string]struct {
3709+
msgs []replicationShardMessages
3710+
want []replicationShardMessages
3711+
}{
3712+
"empty": {},
3713+
"multiple nil, non nil earliestCreationTime": {
3714+
msgs: []replicationShardMessages{
3715+
{earliestCreationTime: nil},
3716+
{earliestCreationTime: nil},
3717+
{earliestCreationTime: common.Int64Ptr(20)},
3718+
{earliestCreationTime: common.Int64Ptr(10)},
3719+
},
3720+
want: []replicationShardMessages{
3721+
{earliestCreationTime: common.Int64Ptr(10)},
3722+
{earliestCreationTime: common.Int64Ptr(20)},
3723+
{earliestCreationTime: nil},
3724+
{earliestCreationTime: nil},
3725+
},
3726+
},
3727+
"multiple nil, non nil same earliestCreationTime, different size": {
3728+
msgs: []replicationShardMessages{
3729+
{earliestCreationTime: nil},
3730+
{earliestCreationTime: nil},
3731+
{earliestCreationTime: common.Int64Ptr(100), size: 50},
3732+
{earliestCreationTime: common.Int64Ptr(100), size: 30},
3733+
{earliestCreationTime: common.Int64Ptr(20)},
3734+
},
3735+
want: []replicationShardMessages{
3736+
{earliestCreationTime: common.Int64Ptr(20)},
3737+
{earliestCreationTime: common.Int64Ptr(100), size: 30},
3738+
{earliestCreationTime: common.Int64Ptr(100), size: 50},
3739+
{earliestCreationTime: nil},
3740+
{earliestCreationTime: nil},
3741+
},
3742+
},
3743+
} {
3744+
t.Run(name, func(t *testing.T) {
3745+
sortReplicationShardMessages(c.msgs)
3746+
assert.Equal(t, c.want, c.msgs)
3747+
})
3748+
}
3749+
}

0 commit comments

Comments
 (0)