Files
nats.go/jetstream/message.go
Piotr Piotrowski e7ab93ecb8 Add ordered consumer, FetchBytes and Next, rework options
Signed-off-by: Piotr Piotrowski <piotr@synadia.com>
2023-05-23 12:03:02 +02:00

330 lines
9.1 KiB
Go

// Copyright 2020-2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jetstream
import (
"bytes"
"context"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/internal/parser"
)
type (
// Msg contains methods to operate on a JetStream message
// Metadata, Data, Headers, Subject and Reply can be used to retrieve the specific parts of the underlying message
// Ack, DoubleAck, Nak, InProgress and Term are various flavors of ack requests
Msg interface {
// Metadata returns [MsgMetadata] for a JetStream message
Metadata() (*MsgMetadata, error)
// Data returns the message body
Data() []byte
// Headers returns a map of headers for a message
Headers() nats.Header
// Subject returns a subject on which a message is published
Subject() string
// Reply returns a reply subject for a message
Reply() string
// Ack acknowledges a message
// This tells the server that the message was successfully processed and it can move on to the next message
Ack() error
// DoubleAck acknowledges a message and waits for ack from server
DoubleAck(context.Context) error
// Nak negatively acknowledges a message
// This tells the server to redeliver the message
Nak(...NakOpt) error
// InProgress tells the server that this message is being worked on
// It resets the redelivery timer on the server
InProgress() error
// Term tells the server to not redeliver this message, regardless of the value of nats.MaxDeliver
Term() error
}
// MsgMetadata is the JetStream metadata associated with received messages.
MsgMetadata struct {
Sequence SequencePair
NumDelivered uint64
NumPending uint64
Timestamp time.Time
Stream string
Consumer string
Domain string
}
// SequencePair includes the consumer and stream sequence info from a JetStream consumer.
SequencePair struct {
Consumer uint64 `json:"consumer_seq"`
Stream uint64 `json:"stream_seq"`
}
jetStreamMsg struct {
msg *nats.Msg
ackd bool
js *jetStream
sync.Mutex
}
ackOpts struct {
nakDelay time.Duration
}
AckOpt func(*ackOpts) error
NakOpt func(*ackOpts) error
ackType []byte
)
const (
controlMsg = "100"
badRequest = "400"
noMessages = "404"
reqTimeout = "408"
maxBytesExceeded = "409"
noResponders = "503"
)
const (
MsgIDHeader = "Nats-Msg-Id"
ExpectedStreamHeader = "Nats-Expected-Stream"
ExpectedLastSeqHeader = "Nats-Expected-Last-Sequence"
ExpectedLastSubjSeqHeader = "Nats-Expected-Last-Subject-Sequence"
ExpectedLastMsgIDHeader = "Nats-Expected-Last-Msg-Id"
MsgRollup = "Nats-Rollup"
)
// Rollups, can be subject only or all messages.
const (
MsgRollupSubject = "sub"
MsgRollupAll = "all"
)
var (
ackAck ackType = []byte("+ACK")
ackNak ackType = []byte("-NAK")
ackProgress ackType = []byte("+WPI")
ackTerm ackType = []byte("+TERM")
)
// Metadata returns [MsgMetadata] for a JetStream message
func (m *jetStreamMsg) Metadata() (*MsgMetadata, error) {
if err := m.checkReply(); err != nil {
return nil, err
}
tokens, err := parser.GetMetadataFields(m.msg.Reply)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrNotJSMessage, err)
}
meta := &MsgMetadata{
Domain: tokens[parser.AckDomainTokenPos],
NumDelivered: parser.ParseNum(tokens[parser.AckNumDeliveredTokenPos]),
NumPending: parser.ParseNum(tokens[parser.AckNumPendingTokenPos]),
Timestamp: time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))),
Stream: tokens[parser.AckStreamTokenPos],
Consumer: tokens[parser.AckConsumerTokenPos],
}
meta.Sequence.Stream = parser.ParseNum(tokens[parser.AckStreamSeqTokenPos])
meta.Sequence.Consumer = parser.ParseNum(tokens[parser.AckConsumerSeqTokenPos])
return meta, nil
}
// Data returns the message body
func (m *jetStreamMsg) Data() []byte {
return m.msg.Data
}
// Headers returns a map of headers for a message
func (m *jetStreamMsg) Headers() nats.Header {
return m.msg.Header
}
// Subject reutrns a subject on which a message is published
func (m *jetStreamMsg) Subject() string {
return m.msg.Subject
}
// Reply reutrns a reply subject for a JetStream message
func (m *jetStreamMsg) Reply() string {
return m.msg.Reply
}
// Ack acknowledges a message
// This tells the server that the message was successfully processed and it can move on to the next message
func (m *jetStreamMsg) Ack() error {
return m.ackReply(context.Background(), ackAck, false, ackOpts{})
}
// DoubleAck acknowledges a message and waits for ack from server
func (m *jetStreamMsg) DoubleAck(ctx context.Context) error {
return m.ackReply(ctx, ackAck, true, ackOpts{})
}
// Nak negatively acknowledges a message
// This tells the server to redeliver the message
// Nak() can be supplied with following options:
// - WithNakDelay() - specify the duration after which the mesage should be redelivered
func (m *jetStreamMsg) Nak(opts ...NakOpt) error {
var o ackOpts
for _, opt := range opts {
if err := opt(&o); err != nil {
return err
}
}
return m.ackReply(context.Background(), ackNak, false, o)
}
// InProgress tells the server that this message is being worked on
// It resets the redelivery timer on the server
func (m *jetStreamMsg) InProgress() error {
return m.ackReply(context.Background(), ackProgress, false, ackOpts{})
}
// Term tells the server to not redeliver this message, regardless of the value of nats.MaxDeliver
func (m *jetStreamMsg) Term() error {
return m.ackReply(context.Background(), ackTerm, false, ackOpts{})
}
func (m *jetStreamMsg) ackReply(ctx context.Context, ackType ackType, sync bool, opts ackOpts) error {
err := m.checkReply()
if err != nil {
return err
}
m.Lock()
if m.ackd {
return ErrMsgAlreadyAckd
}
m.Unlock()
if sync {
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
return nats.ErrNoDeadlineContext
}
}
var body []byte
if opts.nakDelay > 0 {
body = []byte(fmt.Sprintf("%s {\"delay\": %d}", ackType, opts.nakDelay.Nanoseconds()))
} else {
body = ackType
}
if sync {
_, err = m.js.conn.RequestWithContext(ctx, m.msg.Reply, body)
} else {
err = m.js.conn.Publish(m.msg.Reply, body)
}
if err != nil {
return err
}
// Mark that the message has been acked unless it is ackProgress
// which can be sent many times.
if !bytes.Equal(ackType, ackProgress) {
m.Lock()
m.ackd = true
m.Unlock()
}
return nil
}
func (m *jetStreamMsg) checkReply() error {
if m == nil || m.msg.Sub == nil {
return ErrMsgNotBound
}
if m.msg.Reply == "" {
return ErrMsgNoReply
}
return nil
}
// Returns if the given message is a user message or not, and if
// checkSts() is true, returns appropriate error based on the
// content of the status (404, etc..)
func checkMsg(msg *nats.Msg) (bool, error) {
// If payload or no header, consider this a user message
if len(msg.Data) > 0 || len(msg.Header) == 0 {
return true, nil
}
// Look for status header
val := msg.Header.Get("Status")
descr := msg.Header.Get("Description")
// If not present, then this is considered a user message
if val == "" {
return true, nil
}
switch val {
case badRequest:
return false, ErrBadRequest
case noResponders:
return false, nats.ErrNoResponders
case noMessages:
// 404 indicates that there are no messages.
return false, ErrNoMessages
case reqTimeout:
return false, nats.ErrTimeout
case controlMsg:
return false, nil
case maxBytesExceeded:
if strings.Contains(strings.ToLower(descr), "message size exceeds maxbytes") {
return false, ErrMaxBytesExceeded
}
if strings.Contains(strings.ToLower(descr), "consumer deleted") {
return false, ErrConsumerDeleted
}
if strings.Contains(strings.ToLower(descr), "leadership change") {
return false, ErrConsumerLeadershipChanged
}
}
return false, fmt.Errorf("nats: %s", msg.Header.Get("Description"))
}
func parsePending(msg *nats.Msg) (int, int, error) {
msgsLeftStr := msg.Header.Get("Nats-Pending-Messages")
var msgsLeft int
var err error
if msgsLeftStr != "" {
msgsLeft, err = strconv.Atoi(msgsLeftStr)
if err != nil {
return 0, 0, fmt.Errorf("nats: invalid format of Nats-Pending-Messages")
}
}
bytesLeftStr := msg.Header.Get("Nats-Pending-Bytes")
var bytesLeft int
if bytesLeftStr != "" {
bytesLeft, err = strconv.Atoi(bytesLeftStr)
if err != nil {
return 0, 0, fmt.Errorf("nats: invalid format of Nats-Pending-Bytes")
}
}
return msgsLeft, bytesLeft, nil
}
// toJSMsg converts core [nats.Msg] to [jetStreamMsg], exposing JetStream-specific operations
func (js *jetStream) toJSMsg(msg *nats.Msg) *jetStreamMsg {
return &jetStreamMsg{
msg: msg,
js: js,
}
}