489 lines
15 KiB
Go
489 lines
15 KiB
Go
// Copyright 2022-2025 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"
|
|
"errors"
|
|
"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, NakWithDelay, 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 was published/received.
|
|
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 reply from the server.
|
|
// While it impacts performance, it is useful for scenarios where
|
|
// message loss is not acceptable.
|
|
DoubleAck(context.Context) error
|
|
|
|
// Nak negatively acknowledges a message. This tells the server to
|
|
// redeliver the message.
|
|
//
|
|
// Nak does not adhere to AckWait or Backoff configured on the consumer
|
|
// and triggers instant redelivery. For a delayed redelivery, use
|
|
// NakWithDelay.
|
|
Nak() error
|
|
|
|
// NakWithDelay negatively acknowledges a message. This tells the server
|
|
// to redeliver the message after the given delay.
|
|
NakWithDelay(delay time.Duration) 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 MaxDeliver.
|
|
Term() error
|
|
|
|
// TermWithReason tells the server to not redeliver this message, regardless of
|
|
// the value of MaxDeliver. The provided reason will be included in JetStream
|
|
// advisory event sent by the server.
|
|
//
|
|
// Note: This will only work with JetStream servers >= 2.10.4.
|
|
// For older servers, TermWithReason will be ignored by the server and the message
|
|
// will not be terminated.
|
|
TermWithReason(reason string) error
|
|
}
|
|
|
|
// MsgMetadata is the JetStream metadata associated with received messages.
|
|
MsgMetadata struct {
|
|
// Sequence is the sequence information for the message.
|
|
Sequence SequencePair
|
|
|
|
// NumDelivered is the number of times this message was delivered to the
|
|
// consumer.
|
|
NumDelivered uint64
|
|
|
|
// NumPending is the number of messages that match the consumer's
|
|
// filter, but have not been delivered yet.
|
|
NumPending uint64
|
|
|
|
// Timestamp is the time the message was originally stored on a stream.
|
|
Timestamp time.Time
|
|
|
|
// Stream is the stream name this message is stored on.
|
|
Stream string
|
|
|
|
// Consumer is the consumer name this message was delivered to.
|
|
Consumer string
|
|
|
|
// Domain is the domain this message was received on.
|
|
Domain string
|
|
}
|
|
|
|
// SequencePair includes the consumer and stream sequence numbers for a
|
|
// message.
|
|
SequencePair struct {
|
|
// Consumer is the consumer sequence number for message deliveries. This
|
|
// is the total number of messages the consumer has seen (including
|
|
// redeliveries).
|
|
Consumer uint64 `json:"consumer_seq"`
|
|
|
|
// Stream is the stream sequence number for a message.
|
|
Stream uint64 `json:"stream_seq"`
|
|
}
|
|
|
|
jetStreamMsg struct {
|
|
msg *nats.Msg
|
|
ackd bool
|
|
js *jetStream
|
|
sync.Mutex
|
|
}
|
|
|
|
ackOpts struct {
|
|
nakDelay time.Duration
|
|
termReason string
|
|
}
|
|
|
|
ackType []byte
|
|
)
|
|
|
|
const (
|
|
statusControlMsg = "100"
|
|
statusBadRequest = "400"
|
|
statusNoMsgs = "404"
|
|
statusTimeout = "408"
|
|
statusConflict = "409"
|
|
statusNoResponders = "503"
|
|
statusPinIdMismatch = "423"
|
|
|
|
fcRequestDescr = "flowcontrol request"
|
|
idleHeartbeatDescr = "idle heartbeat"
|
|
consumerDeleted = "consumer deleted"
|
|
leadershipChange = "leadership change"
|
|
maxBytesExceeded = "message size exceeds maxbytes"
|
|
batchCompleted = "batch completed"
|
|
serverShutdown = "server shutdown"
|
|
)
|
|
|
|
// Headers used when publishing messages.
|
|
const (
|
|
// MsgIdHeader is used to specify a user-defined message ID. It can be used
|
|
// e.g. for deduplication in conjunction with the Duplicates duration on
|
|
// ConsumerConfig or to provide optimistic concurrency safety together with
|
|
// [ExpectedLastMsgIDHeader].
|
|
//
|
|
// This can be set when publishing messages using [WithMsgID] option.
|
|
MsgIDHeader = "Nats-Msg-Id"
|
|
|
|
// ExpectedStreamHeader contains stream name and is used to assure that the
|
|
// published message is received by expected stream. Server will reject the
|
|
// message if it is not the case.
|
|
//
|
|
// This can be set when publishing messages using [WithExpectStream] option.
|
|
ExpectedStreamHeader = "Nats-Expected-Stream"
|
|
|
|
// ExpectedLastSeqHeader contains the expected last sequence number of the
|
|
// stream and can be used to apply optimistic concurrency control at stream
|
|
// level. Server will reject the message if it is not the case.
|
|
//
|
|
// This can be set when publishing messages using [WithExpectLastSequence]
|
|
// option.
|
|
ExpectedLastSeqHeader = "Nats-Expected-Last-Sequence"
|
|
|
|
// ExpectedLastSubjSeqHeader contains the expected last sequence number on
|
|
// the subject and can be used to apply optimistic concurrency control at
|
|
// subject level. Server will reject the message if it is not the case.
|
|
//
|
|
// This can be set when publishing messages using
|
|
// [WithExpectLastSequencePerSubject] option.
|
|
ExpectedLastSubjSeqHeader = "Nats-Expected-Last-Subject-Sequence"
|
|
|
|
// ExpectedLastSubjSeqSubjHeader contains the subject for which the
|
|
// expected last sequence number is set. This is used together with
|
|
// [ExpectedLastSubjSeqHeader] to apply optimistic concurrency control at
|
|
// subject level. Server will reject the message if it is not the case.
|
|
ExpectedLastSubjSeqSubjHeader = "Nats-Expected-Last-Subject-Sequence-Subject"
|
|
|
|
// ExpectedLastMsgIDHeader contains the expected last message ID on the
|
|
// subject and can be used to apply optimistic concurrency control at
|
|
// stream level. Server will reject the message if it is not the case.
|
|
//
|
|
// This can be set when publishing messages using [WithExpectLastMsgID]
|
|
// option.
|
|
ExpectedLastMsgIDHeader = "Nats-Expected-Last-Msg-Id"
|
|
|
|
// MsgTTLHeader is used to specify the TTL for a specific message. This will
|
|
// override the default TTL for the stream.
|
|
MsgTTLHeader = "Nats-TTL"
|
|
|
|
// MsgRollup is used to apply a purge of all prior messages in the stream
|
|
// ("all") or at the subject ("sub") before this message.
|
|
MsgRollup = "Nats-Rollup"
|
|
|
|
// MarkerReasonHeader is used to specify a reason for message deletion.
|
|
MarkerReasonHeader = "Nats-Marker-Reason"
|
|
)
|
|
|
|
// Headers for republished messages and direct gets. Those headers are set by
|
|
// the server and should not be set by the client.
|
|
const (
|
|
// StreamHeader contains the stream name the message was republished from or
|
|
// the stream name the message was retrieved from using direct get.
|
|
StreamHeader = "Nats-Stream"
|
|
|
|
// SequenceHeader contains the original sequence number of the message.
|
|
SequenceHeader = "Nats-Sequence"
|
|
|
|
// TimeStampHeaer contains the original timestamp of the message.
|
|
TimeStampHeaer = "Nats-Time-Stamp"
|
|
|
|
// SubjectHeader contains the original subject the message was published to.
|
|
SubjectHeader = "Nats-Subject"
|
|
|
|
// LastSequenceHeader contains the last sequence of the message having the
|
|
// same subject, otherwise zero if this is the first message for the
|
|
// subject.
|
|
LastSequenceHeader = "Nats-Last-Sequence"
|
|
)
|
|
|
|
// Rollups, can be subject only or all messages.
|
|
const (
|
|
// MsgRollupSubject is used to purge all messages before this message on the
|
|
// message subject.
|
|
MsgRollupSubject = "sub"
|
|
|
|
// MsgRollupAll is used to purge all messages before this message on the
|
|
// stream.
|
|
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 returns a subject on which a message was published/received.
|
|
func (m *jetStreamMsg) Subject() string {
|
|
return m.msg.Subject
|
|
}
|
|
|
|
// Reply returns 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 reply from the server.
|
|
// While it impacts performance, it is useful for scenarios where
|
|
// message loss is not acceptable.
|
|
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.
|
|
func (m *jetStreamMsg) Nak() error {
|
|
return m.ackReply(context.Background(), ackNak, false, ackOpts{})
|
|
}
|
|
|
|
// NakWithDelay negatively acknowledges a message. This tells the server
|
|
// to redeliver the message after the given delay.
|
|
func (m *jetStreamMsg) NakWithDelay(delay time.Duration) error {
|
|
return m.ackReply(context.Background(), ackNak, false, ackOpts{nakDelay: delay})
|
|
}
|
|
|
|
// 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 MaxDeliver.
|
|
func (m *jetStreamMsg) Term() error {
|
|
return m.ackReply(context.Background(), ackTerm, false, ackOpts{})
|
|
}
|
|
|
|
// TermWithReason tells the server to not redeliver this message, regardless of
|
|
// the value of MaxDeliver. The provided reason will be included in JetStream
|
|
// advisory event sent by the server.
|
|
//
|
|
// Note: This will only work with JetStream servers >= 2.10.4.
|
|
// For older servers, TermWithReason will be ignored by the server and the message
|
|
// will not be terminated.
|
|
func (m *jetStreamMsg) TermWithReason(reason string) error {
|
|
return m.ackReply(context.Background(), ackTerm, false, ackOpts{termReason: reason})
|
|
}
|
|
|
|
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 {
|
|
m.Unlock()
|
|
return ErrMsgAlreadyAckd
|
|
}
|
|
m.Unlock()
|
|
|
|
if sync {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = m.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
}
|
|
|
|
var body []byte
|
|
if opts.nakDelay > 0 {
|
|
body = []byte(fmt.Sprintf("%s {\"delay\": %d}", ackType, opts.nakDelay.Nanoseconds()))
|
|
} else if opts.termReason != "" {
|
|
body = []byte(fmt.Sprintf("%s %s", ackType, opts.termReason))
|
|
} 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
|
|
}
|
|
|
|
// checkMsg returns whether the given message is a user message or a control message.
|
|
// If the status header is present, it returns an appropriate error based
|
|
// on the status code (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 statusBadRequest:
|
|
return false, ErrBadRequest
|
|
case statusNoResponders:
|
|
return false, nats.ErrNoResponders
|
|
case statusNoMsgs:
|
|
// 404 indicates that there are no messages.
|
|
return false, ErrNoMessages
|
|
case statusTimeout:
|
|
return false, nats.ErrTimeout
|
|
case statusControlMsg:
|
|
return false, nil
|
|
case statusPinIdMismatch:
|
|
return false, ErrPinIDMismatch
|
|
case statusConflict:
|
|
if strings.Contains(strings.ToLower(descr), maxBytesExceeded) {
|
|
return false, ErrMaxBytesExceeded
|
|
}
|
|
if strings.Contains(strings.ToLower(descr), batchCompleted) {
|
|
return false, ErrBatchCompleted
|
|
}
|
|
if strings.Contains(strings.ToLower(descr), consumerDeleted) {
|
|
return false, ErrConsumerDeleted
|
|
}
|
|
if strings.Contains(strings.ToLower(descr), leadershipChange) {
|
|
return false, ErrConsumerLeadershipChanged
|
|
}
|
|
if strings.Contains(strings.ToLower(descr), serverShutdown) {
|
|
return false, ErrServerShutdown
|
|
}
|
|
}
|
|
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, errors.New("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, errors.New("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,
|
|
}
|
|
}
|