mirror of
https://github.com/nats-io/nats.go.git
synced 2025-10-28 02:42:14 +08:00
330 lines
9.1 KiB
Go
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,
|
|
}
|
|
}
|