milvus/internal/distributed/streaming/util.go

160 lines
4.8 KiB
Go

package streaming
import (
"context"
"sync"
"github.com/milvus-io/milvus/pkg/v2/streaming/util/message"
"github.com/milvus-io/milvus/pkg/v2/streaming/util/types"
)
type (
AppendResponses = types.AppendResponses
AppendResponse = types.AppendResponse
)
// AppendMessagesToWAL appends messages to the wal.
// It it a helper utility function to append messages to the wal.
// If the messages is belong to one vchannel, it will be sent as a transaction.
// Otherwise, it will be sent as individual messages.
// !!! This function do not promise the atomicity and deliver order of the messages appending.
func (u *walAccesserImpl) AppendMessages(ctx context.Context, msgs ...message.MutableMessage) AppendResponses {
assertValidMessage(msgs...)
// dispatch the messages into different vchannel.
dispatchedMessages, indexes := u.dispatchMessages(msgs...)
// If only one vchannel, append it directly without other goroutine.
if len(dispatchedMessages) == 1 {
return u.appendToVChannel(ctx, msgs[0].VChannel(), msgs...)
}
// Otherwise append the messages concurrently.
mu := &sync.Mutex{}
resp := types.NewAppendResponseN(len(msgs))
wg := &sync.WaitGroup{}
wg.Add(len(dispatchedMessages))
for vchannel, msgs := range dispatchedMessages {
vchannel := vchannel
msgs := msgs
idxes := indexes[vchannel]
u.dispatchExecutionPool.Submit(func() (struct{}, error) {
defer wg.Done()
singleResp := u.appendToVChannel(ctx, vchannel, msgs...)
mu.Lock()
for i, idx := range idxes {
resp.FillResponseAtIdx(singleResp.Responses[i], idx)
}
mu.Unlock()
return struct{}{}, nil
})
}
wg.Wait()
return resp
}
// AppendMessagesWithOption appends messages to the wal with the given option.
func (u *walAccesserImpl) AppendMessagesWithOption(ctx context.Context, opts AppendOption, msgs ...message.MutableMessage) AppendResponses {
for _, msg := range msgs {
applyOpt(msg, opts)
}
return u.AppendMessages(ctx, msgs...)
}
// dispatchMessages dispatches the messages into different vchannel.
func (u *walAccesserImpl) dispatchMessages(msgs ...message.MutableMessage) (map[string][]message.MutableMessage, map[string][]int) {
dispatchedMessages := make(map[string][]message.MutableMessage, 0)
indexes := make(map[string][]int, 0)
for idx, msg := range msgs {
vchannel := msg.VChannel()
if _, ok := dispatchedMessages[vchannel]; !ok {
dispatchedMessages[vchannel] = make([]message.MutableMessage, 0)
indexes[vchannel] = make([]int, 0)
}
dispatchedMessages[vchannel] = append(dispatchedMessages[vchannel], msg)
indexes[vchannel] = append(indexes[vchannel], idx)
}
return dispatchedMessages, indexes
}
// appendToVChannel appends the messages to the specified vchannel.
func (u *walAccesserImpl) appendToVChannel(ctx context.Context, vchannel string, msgs ...message.MutableMessage) AppendResponses {
if len(msgs) == 0 {
return types.NewAppendResponseN(0)
}
resp := types.NewAppendResponseN(len(msgs))
// if only one message here, append it directly, no more goroutine needed.
// at most time, there's only one message here.
// TODO: only the partition-key with high partition will generate many message in one time on the same pchannel,
// we should optimize the message-format, make it into one; but not the goroutine count.
if len(msgs) == 1 {
appendResult, err := u.appendToWAL(ctx, msgs[0])
resp.FillResponseAtIdx(AppendResponse{
AppendResult: appendResult,
Error: err,
}, 0)
return resp
}
// Otherwise, we start a transaction to append the messages.
// The transaction will be committed when all messages are appended.
txn, err := u.Txn(ctx, TxnOption{
VChannel: vchannel,
})
if err != nil {
resp.FillAllError(err)
return resp
}
// concurrent produce here.
wg := sync.WaitGroup{}
wg.Add(len(msgs))
mu := sync.Mutex{}
for i, msg := range msgs {
i := i
msg := msg
u.appendExecutionPool.Submit(func() (struct{}, error) {
defer wg.Done()
if err := txn.Append(ctx, msg); err != nil {
mu.Lock()
resp.FillResponseAtIdx(AppendResponse{
Error: err,
}, i)
mu.Unlock()
}
return struct{}{}, nil
})
}
wg.Wait()
// if there's any error, rollback the transaction.
// and fill the error with the first error.
if err := resp.UnwrapFirstError(); err != nil {
_ = txn.Rollback(ctx) // rollback failure can be ignored.
resp.FillAllError(err)
return resp
}
// commit the transaction and fill the response.
appendResult, err := txn.Commit(ctx)
resp.FillAllResponse(AppendResponse{
AppendResult: appendResult,
Error: err,
})
return resp
}
// applyOpt applies the append options to the message.
func applyOpt(msg message.MutableMessage, opts ...AppendOption) message.MutableMessage {
if len(opts) == 0 {
return msg
}
if opts[0].BarrierTimeTick > 0 {
msg = msg.WithBarrierTimeTick(opts[0].BarrierTimeTick)
}
return msg
}