828 lines
25 KiB
Go
828 lines
25 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 (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nuid"
|
|
)
|
|
|
|
type (
|
|
// Stream contains CRUD methods on a consumer via [ConsumerManager], as well
|
|
// as operations on an existing stream. It allows fetching and removing
|
|
// messages from a stream, as well as purging a stream.
|
|
Stream interface {
|
|
ConsumerManager
|
|
|
|
// Info returns StreamInfo from the server.
|
|
Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, error)
|
|
|
|
// CachedInfo returns ConsumerInfo currently cached on this stream.
|
|
// This method does not perform any network requests. The cached
|
|
// StreamInfo is updated on every call to Info and Update.
|
|
CachedInfo() *StreamInfo
|
|
|
|
// Purge removes messages from a stream. It is a destructive operation.
|
|
// Use with caution. See StreamPurgeOpt for available options.
|
|
Purge(ctx context.Context, opts ...StreamPurgeOpt) error
|
|
|
|
// GetMsg retrieves a raw stream message stored in JetStream by sequence number.
|
|
GetMsg(ctx context.Context, seq uint64, opts ...GetMsgOpt) (*RawStreamMsg, error)
|
|
|
|
// GetLastMsgForSubject retrieves the last raw stream message stored in
|
|
// JetStream on a given subject.
|
|
GetLastMsgForSubject(ctx context.Context, subject string) (*RawStreamMsg, error)
|
|
|
|
// DeleteMsg deletes a message from a stream.
|
|
// On the server, the message is marked as erased, but not overwritten.
|
|
DeleteMsg(ctx context.Context, seq uint64) error
|
|
|
|
// SecureDeleteMsg deletes a message from a stream. The deleted message
|
|
// is overwritten with random data. As a result, this operation is slower
|
|
// than DeleteMsg.
|
|
SecureDeleteMsg(ctx context.Context, seq uint64) error
|
|
}
|
|
|
|
// ConsumerManager provides CRUD API for managing consumers. It is
|
|
// available as a part of [Stream] interface. CreateConsumer,
|
|
// UpdateConsumer, CreateOrUpdateConsumer and Consumer methods return a
|
|
// [Consumer] interface, allowing to operate on a consumer (e.g. consume
|
|
// messages).
|
|
ConsumerManager interface {
|
|
// CreateOrUpdateConsumer creates a pull consumer on a given stream with
|
|
// given config. If consumer already exists, it will be updated (if
|
|
// possible). Consumer interface is returned, allowing to operate on a
|
|
// consumer (e.g. fetch messages).
|
|
CreateOrUpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error)
|
|
|
|
// CreateConsumer creates a pull consumer on a given stream with given
|
|
// config. If consumer already exists and the provided configuration
|
|
// differs from its configuration, ErrConsumerExists is returned. If the
|
|
// provided configuration is the same as the existing consumer, the
|
|
// existing consumer is returned. Consumer interface is returned,
|
|
// allowing to operate on a consumer (e.g. fetch messages).
|
|
CreateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error)
|
|
|
|
// UpdateConsumer updates an existing pull consumer. If consumer does not
|
|
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
|
|
// returned, allowing to operate on a consumer (e.g. fetch messages).
|
|
UpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error)
|
|
|
|
// OrderedConsumer returns an OrderedConsumer instance. OrderedConsumer
|
|
// are managed by the library and provide a simple way to consume
|
|
// messages from a stream. Ordered consumers are ephemeral in-memory
|
|
// pull consumers and are resilient to deletes and restarts.
|
|
OrderedConsumer(ctx context.Context, cfg OrderedConsumerConfig) (Consumer, error)
|
|
|
|
// Consumer returns an interface to an existing consumer, allowing processing
|
|
// of messages. If consumer does not exist, ErrConsumerNotFound is
|
|
// returned.
|
|
//
|
|
// It returns ErrNotPullConsumer if the consumer is not a pull consumer (deliver subject is not set).
|
|
Consumer(ctx context.Context, consumer string) (Consumer, error)
|
|
|
|
// DeleteConsumer removes a consumer with given name from a stream.
|
|
// If consumer does not exist, ErrConsumerNotFound is returned.
|
|
DeleteConsumer(ctx context.Context, consumer string) error
|
|
|
|
// PauseConsumer pauses a consumer.
|
|
PauseConsumer(ctx context.Context, consumer string, pauseUntil time.Time) (*ConsumerPauseResponse, error)
|
|
|
|
// ResumeConsumer resumes a consumer.
|
|
ResumeConsumer(ctx context.Context, consumer string) (*ConsumerPauseResponse, error)
|
|
|
|
// ListConsumers returns ConsumerInfoLister enabling iterating over a
|
|
// channel of consumer infos.
|
|
ListConsumers(context.Context) ConsumerInfoLister
|
|
|
|
// ConsumerNames returns a ConsumerNameLister enabling iterating over a
|
|
// channel of consumer names.
|
|
ConsumerNames(context.Context) ConsumerNameLister
|
|
|
|
// UnpinConsumer unpins the currently pinned client for a consumer for the given group name.
|
|
// If consumer does not exist, ErrConsumerNotFound is returned.
|
|
UnpinConsumer(ctx context.Context, consumer string, group string) error
|
|
|
|
// CreateOrUpdatePushConsumer creates a push consumer on a given stream with
|
|
// given config. If consumer already exists, it will be updated (if
|
|
// possible). Consumer interface is returned, allowing to consume messages.
|
|
CreateOrUpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error)
|
|
|
|
// CreatePushConsumer creates a push consumer on a given stream with given
|
|
// config. If consumer already exists and the provided configuration
|
|
// differs from its configuration, ErrConsumerExists is returned. If the
|
|
// provided configuration is the same as the existing consumer, the
|
|
// existing consumer is returned. Consumer interface is returned,
|
|
// allowing to consume messages.
|
|
CreatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error)
|
|
|
|
// UpdatePushConsumer updates an existing push consumer. If consumer does not
|
|
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
|
|
// returned, allowing to consume messages.
|
|
UpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error)
|
|
|
|
// PushConsumer returns an interface to an existing push consumer, allowing processing
|
|
// of messages. If consumer does not exist, ErrConsumerNotFound is
|
|
// returned.
|
|
//
|
|
// It returns ErrNotPushConsumer if the consumer is not a push consumer (deliver subject is not set).
|
|
PushConsumer(ctx context.Context, consumer string) (PushConsumer, error)
|
|
}
|
|
|
|
RawStreamMsg struct {
|
|
Subject string
|
|
Sequence uint64
|
|
Header nats.Header
|
|
Data []byte
|
|
Time time.Time
|
|
}
|
|
|
|
stream struct {
|
|
name string
|
|
info *StreamInfo
|
|
js *jetStream
|
|
}
|
|
|
|
// StreamInfoOpt is a function setting options for [Stream.Info]
|
|
StreamInfoOpt func(*streamInfoRequest) error
|
|
|
|
streamInfoRequest struct {
|
|
apiPagedRequest
|
|
DeletedDetails bool `json:"deleted_details,omitempty"`
|
|
SubjectFilter string `json:"subjects_filter,omitempty"`
|
|
}
|
|
|
|
consumerInfoResponse struct {
|
|
apiResponse
|
|
*ConsumerInfo
|
|
}
|
|
|
|
// StreamPurgeOpt is a function setting options for [Stream.Purge]
|
|
StreamPurgeOpt func(*StreamPurgeRequest) error
|
|
|
|
// StreamPurgeRequest is an API request body to purge a stream.
|
|
|
|
StreamPurgeRequest struct {
|
|
// Purge up to but not including sequence.
|
|
Sequence uint64 `json:"seq,omitempty"`
|
|
// Subject to match against messages for the purge command.
|
|
Subject string `json:"filter,omitempty"`
|
|
// Number of messages to keep.
|
|
Keep uint64 `json:"keep,omitempty"`
|
|
}
|
|
|
|
streamPurgeResponse struct {
|
|
apiResponse
|
|
Success bool `json:"success,omitempty"`
|
|
Purged uint64 `json:"purged"`
|
|
}
|
|
|
|
consumerDeleteResponse struct {
|
|
apiResponse
|
|
Success bool `json:"success,omitempty"`
|
|
}
|
|
|
|
consumerPauseRequest struct {
|
|
PauseUntil *time.Time `json:"pause_until,omitempty"`
|
|
}
|
|
|
|
ConsumerPauseResponse struct {
|
|
// Paused is true if the consumer is paused.
|
|
Paused bool `json:"paused"`
|
|
// PauseUntil is the time until the consumer is paused.
|
|
PauseUntil time.Time `json:"pause_until"`
|
|
// PauseRemaining is the time remaining until the consumer is paused.
|
|
PauseRemaining time.Duration `json:"pause_remaining,omitempty"`
|
|
}
|
|
|
|
consumerPauseApiResponse struct {
|
|
apiResponse
|
|
ConsumerPauseResponse
|
|
}
|
|
|
|
// GetMsgOpt is a function setting options for [Stream.GetMsg]
|
|
GetMsgOpt func(*apiMsgGetRequest) error
|
|
|
|
apiMsgGetRequest struct {
|
|
Seq uint64 `json:"seq,omitempty"`
|
|
LastFor string `json:"last_by_subj,omitempty"`
|
|
NextFor string `json:"next_by_subj,omitempty"`
|
|
}
|
|
|
|
// apiMsgGetResponse is the response for a Stream get request.
|
|
apiMsgGetResponse struct {
|
|
apiResponse
|
|
Message *storedMsg `json:"message,omitempty"`
|
|
}
|
|
|
|
// storedMsg is a raw message stored in JetStream.
|
|
storedMsg struct {
|
|
Subject string `json:"subject"`
|
|
Sequence uint64 `json:"seq"`
|
|
Header []byte `json:"hdrs,omitempty"`
|
|
Data []byte `json:"data,omitempty"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
msgDeleteRequest struct {
|
|
Seq uint64 `json:"seq"`
|
|
NoErase bool `json:"no_erase,omitempty"`
|
|
}
|
|
|
|
msgDeleteResponse struct {
|
|
apiResponse
|
|
Success bool `json:"success,omitempty"`
|
|
}
|
|
|
|
// ConsumerInfoLister is used to iterate over a channel of consumer infos.
|
|
// Err method can be used to check for errors encountered during iteration.
|
|
// Info channel is always closed and therefore can be used in a range loop.
|
|
ConsumerInfoLister interface {
|
|
Info() <-chan *ConsumerInfo
|
|
Err() error
|
|
}
|
|
|
|
// ConsumerNameLister is used to iterate over a channel of consumer names.
|
|
// Err method can be used to check for errors encountered during iteration.
|
|
// Name channel is always closed and therefore can be used in a range loop.
|
|
ConsumerNameLister interface {
|
|
Name() <-chan string
|
|
Err() error
|
|
}
|
|
|
|
consumerLister struct {
|
|
js *jetStream
|
|
offset int
|
|
pageInfo *apiPaged
|
|
|
|
consumers chan *ConsumerInfo
|
|
names chan string
|
|
err error
|
|
}
|
|
|
|
consumerListResponse struct {
|
|
apiResponse
|
|
apiPaged
|
|
Consumers []*ConsumerInfo `json:"consumers"`
|
|
}
|
|
|
|
consumerNamesResponse struct {
|
|
apiResponse
|
|
apiPaged
|
|
Consumers []string `json:"consumers"`
|
|
}
|
|
|
|
consumerUnpinRequest struct {
|
|
Group string `json:"group"`
|
|
}
|
|
)
|
|
|
|
// CreateOrUpdateConsumer creates a consumer on a given stream with
|
|
// given config. If consumer already exists, it will be updated (if
|
|
// possible). Consumer interface is returned, allowing to operate on a
|
|
// consumer (e.g. fetch messages).
|
|
func (s *stream) CreateOrUpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
|
|
return upsertPullConsumer(ctx, s.js, s.name, cfg, consumerActionCreateOrUpdate)
|
|
}
|
|
|
|
// CreateConsumer creates a consumer on a given stream with given
|
|
// config. If consumer already exists and the provided configuration
|
|
// differs from its configuration, ErrConsumerExists is returned. If the
|
|
// provided configuration is the same as the existing consumer, the
|
|
// existing consumer is returned. Consumer interface is returned,
|
|
// allowing to operate on a consumer (e.g. fetch messages).
|
|
func (s *stream) CreateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
|
|
return upsertPullConsumer(ctx, s.js, s.name, cfg, consumerActionCreate)
|
|
}
|
|
|
|
// UpdateConsumer updates an existing consumer. If consumer does not
|
|
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
|
|
// returned, allowing to operate on a consumer (e.g. fetch messages).
|
|
func (s *stream) UpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
|
|
return upsertPullConsumer(ctx, s.js, s.name, cfg, consumerActionUpdate)
|
|
}
|
|
|
|
// CreateOrUpdatePushConsumer creates a consumer on a given stream with
|
|
// given config. If consumer already exists, it will be updated (if
|
|
// possible). Consumer interface is returned, allowing to consume messages.
|
|
func (s *stream) CreateOrUpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
|
|
return upsertPushConsumer(ctx, s.js, s.name, cfg, consumerActionCreateOrUpdate)
|
|
}
|
|
|
|
// CreatePushConsumer creates a consumer on a given stream with given
|
|
// config. If consumer already exists and the provided configuration
|
|
// differs from its configuration, ErrConsumerExists is returned. If the
|
|
// provided configuration is the same as the existing consumer, the
|
|
// existing consumer is returned. Consumer interface is returned,
|
|
// allowing to consume messages.
|
|
func (s *stream) CreatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
|
|
return upsertPushConsumer(ctx, s.js, s.name, cfg, consumerActionCreate)
|
|
}
|
|
|
|
// UpdatePushConsumer updates an existing consumer. If consumer does not
|
|
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
|
|
// returned, allowing to consume messages.
|
|
func (s *stream) UpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
|
|
return upsertPushConsumer(ctx, s.js, s.name, cfg, consumerActionUpdate)
|
|
}
|
|
|
|
// OrderedConsumer returns an OrderedConsumer instance. OrderedConsumer
|
|
// are managed by the library and provide a simple way to consume
|
|
// messages from a stream. Ordered consumers are ephemeral in-memory
|
|
// pull consumers and are resilient to deletes and restarts.
|
|
func (s *stream) OrderedConsumer(ctx context.Context, cfg OrderedConsumerConfig) (Consumer, error) {
|
|
namePrefix := cfg.NamePrefix
|
|
if namePrefix == "" {
|
|
namePrefix = nuid.Next()
|
|
}
|
|
oc := &orderedConsumer{
|
|
js: s.js,
|
|
cfg: &cfg,
|
|
stream: s.name,
|
|
namePrefix: namePrefix,
|
|
doReset: make(chan struct{}, 1),
|
|
}
|
|
consCfg := oc.getConsumerConfig()
|
|
cons, err := s.CreateOrUpdateConsumer(ctx, *consCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
oc.currentConsumer = cons.(*pullConsumer)
|
|
|
|
return oc, nil
|
|
}
|
|
|
|
// Consumer returns an interface to an existing consumer, allowing processing
|
|
// of messages. If consumer does not exist, ErrConsumerNotFound is
|
|
// returned.
|
|
func (s *stream) Consumer(ctx context.Context, name string) (Consumer, error) {
|
|
return getConsumer(ctx, s.js, s.name, name)
|
|
}
|
|
|
|
func (s *stream) PushConsumer(ctx context.Context, name string) (PushConsumer, error) {
|
|
return getPushConsumer(ctx, s.js, s.name, name)
|
|
}
|
|
|
|
// DeleteConsumer removes a consumer with given name from a stream.
|
|
// If consumer does not exist, ErrConsumerNotFound is returned.
|
|
func (s *stream) DeleteConsumer(ctx context.Context, name string) error {
|
|
return deleteConsumer(ctx, s.js, s.name, name)
|
|
}
|
|
|
|
// PauseConsumer pauses a consumer.
|
|
func (s *stream) PauseConsumer(ctx context.Context, name string, pauseUntil time.Time) (*ConsumerPauseResponse, error) {
|
|
return pauseConsumer(ctx, s.js, s.name, name, &pauseUntil)
|
|
}
|
|
|
|
// ResumeConsumer resumes a consumer.
|
|
func (s *stream) ResumeConsumer(ctx context.Context, name string) (*ConsumerPauseResponse, error) {
|
|
return resumeConsumer(ctx, s.js, s.name, name)
|
|
}
|
|
|
|
// Info returns StreamInfo from the server.
|
|
func (s *stream) Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, error) {
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
var infoReq *streamInfoRequest
|
|
for _, opt := range opts {
|
|
if infoReq == nil {
|
|
infoReq = &streamInfoRequest{}
|
|
}
|
|
if err := opt(infoReq); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var req []byte
|
|
var err error
|
|
var subjectMap map[string]uint64
|
|
var offset int
|
|
|
|
infoSubject := fmt.Sprintf(apiStreamInfoT, s.name)
|
|
var info *StreamInfo
|
|
for {
|
|
if infoReq != nil {
|
|
if infoReq.SubjectFilter != "" {
|
|
if subjectMap == nil {
|
|
subjectMap = make(map[string]uint64)
|
|
}
|
|
infoReq.Offset = offset
|
|
}
|
|
req, err = json.Marshal(infoReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var resp streamInfoResponse
|
|
if _, err = s.js.apiRequestJSON(ctx, infoSubject, &resp, req); err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Error != nil {
|
|
if resp.Error.ErrorCode == JSErrCodeStreamNotFound {
|
|
return nil, ErrStreamNotFound
|
|
}
|
|
return nil, resp.Error
|
|
}
|
|
info = resp.StreamInfo
|
|
var total int
|
|
if resp.Total != 0 {
|
|
total = resp.Total
|
|
}
|
|
if len(resp.StreamInfo.State.Subjects) > 0 {
|
|
for subj, msgs := range resp.StreamInfo.State.Subjects {
|
|
subjectMap[subj] = msgs
|
|
}
|
|
offset = len(subjectMap)
|
|
}
|
|
if total == 0 || total <= offset {
|
|
info.State.Subjects = nil
|
|
// we don't want to store subjects in cache
|
|
cached := *info
|
|
s.info = &cached
|
|
info.State.Subjects = subjectMap
|
|
break
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// CachedInfo returns ConsumerInfo currently cached on this stream.
|
|
// This method does not perform any network requests. The cached
|
|
// StreamInfo is updated on every call to Info and Update.
|
|
func (s *stream) CachedInfo() *StreamInfo {
|
|
return s.info
|
|
}
|
|
|
|
// Purge removes messages from a stream. It is a destructive operation.
|
|
// Use with caution. See StreamPurgeOpt for available options.
|
|
func (s *stream) Purge(ctx context.Context, opts ...StreamPurgeOpt) error {
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
var purgeReq StreamPurgeRequest
|
|
for _, opt := range opts {
|
|
if err := opt(&purgeReq); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var req []byte
|
|
var err error
|
|
req, err = json.Marshal(purgeReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
purgeSubject := fmt.Sprintf(apiStreamPurgeT, s.name)
|
|
|
|
var resp streamPurgeResponse
|
|
if _, err = s.js.apiRequestJSON(ctx, purgeSubject, &resp, req); err != nil {
|
|
return err
|
|
}
|
|
if resp.Error != nil {
|
|
return resp.Error
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMsg retrieves a raw stream message stored in JetStream by sequence number.
|
|
func (s *stream) GetMsg(ctx context.Context, seq uint64, opts ...GetMsgOpt) (*RawStreamMsg, error) {
|
|
req := &apiMsgGetRequest{Seq: seq}
|
|
for _, opt := range opts {
|
|
if err := opt(req); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return s.getMsg(ctx, req)
|
|
}
|
|
|
|
// GetLastMsgForSubject retrieves the last raw stream message stored in
|
|
// JetStream on a given subject subject.
|
|
func (s *stream) GetLastMsgForSubject(ctx context.Context, subject string) (*RawStreamMsg, error) {
|
|
return s.getMsg(ctx, &apiMsgGetRequest{LastFor: subject})
|
|
}
|
|
|
|
func (s *stream) getMsg(ctx context.Context, mreq *apiMsgGetRequest) (*RawStreamMsg, error) {
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
req, err := json.Marshal(mreq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var gmSubj string
|
|
|
|
// handle direct gets
|
|
if s.info.Config.AllowDirect {
|
|
if mreq.LastFor != "" {
|
|
gmSubj = fmt.Sprintf(apiDirectMsgGetLastBySubjectT, s.name, mreq.LastFor)
|
|
r, err := s.js.apiRequest(ctx, gmSubj, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertDirectGetMsgResponseToMsg(r.msg)
|
|
}
|
|
gmSubj = fmt.Sprintf(apiDirectMsgGetT, s.name)
|
|
r, err := s.js.apiRequest(ctx, gmSubj, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertDirectGetMsgResponseToMsg(r.msg)
|
|
}
|
|
|
|
var resp apiMsgGetResponse
|
|
dsSubj := fmt.Sprintf(apiMsgGetT, s.name)
|
|
_, err = s.js.apiRequestJSON(ctx, dsSubj, &resp, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.Error != nil {
|
|
if resp.Error.ErrorCode == JSErrCodeMessageNotFound {
|
|
return nil, ErrMsgNotFound
|
|
}
|
|
return nil, resp.Error
|
|
}
|
|
|
|
msg := resp.Message
|
|
|
|
var hdr nats.Header
|
|
if len(msg.Header) > 0 {
|
|
hdr, err = nats.DecodeHeadersMsg(msg.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &RawStreamMsg{
|
|
Subject: msg.Subject,
|
|
Sequence: msg.Sequence,
|
|
Header: hdr,
|
|
Data: msg.Data,
|
|
Time: msg.Time,
|
|
}, nil
|
|
}
|
|
|
|
func convertDirectGetMsgResponseToMsg(r *nats.Msg) (*RawStreamMsg, error) {
|
|
// Check for 404/408. We would get a no-payload message and a "Status" header
|
|
if len(r.Data) == 0 {
|
|
val := r.Header.Get(statusHdr)
|
|
if val != "" {
|
|
switch val {
|
|
case statusNoMsgs:
|
|
return nil, ErrMsgNotFound
|
|
default:
|
|
desc := r.Header.Get("Description")
|
|
if desc == "" {
|
|
desc = "unable to get message"
|
|
}
|
|
return nil, fmt.Errorf("nats: %s", desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for headers that give us the required information to
|
|
// reconstruct the message.
|
|
if len(r.Header) == 0 {
|
|
return nil, errors.New("nats: response should have headers")
|
|
}
|
|
stream := r.Header.Get(StreamHeader)
|
|
if stream == "" {
|
|
return nil, errors.New("nats: missing stream header")
|
|
}
|
|
|
|
seqStr := r.Header.Get(SequenceHeader)
|
|
if seqStr == "" {
|
|
return nil, errors.New("nats: missing sequence header")
|
|
}
|
|
seq, err := strconv.ParseUint(seqStr, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nats: invalid sequence header '%s': %v", seqStr, err)
|
|
}
|
|
timeStr := r.Header.Get(TimeStampHeaer)
|
|
if timeStr == "" {
|
|
return nil, errors.New("nats: missing timestamp header")
|
|
}
|
|
|
|
tm, err := time.Parse(time.RFC3339Nano, timeStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nats: invalid timestamp header '%s': %v", timeStr, err)
|
|
}
|
|
subj := r.Header.Get(SubjectHeader)
|
|
if subj == "" {
|
|
return nil, errors.New("nats: missing subject header")
|
|
}
|
|
return &RawStreamMsg{
|
|
Subject: subj,
|
|
Sequence: seq,
|
|
Header: r.Header,
|
|
Data: r.Data,
|
|
Time: tm,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteMsg deletes a message from a stream.
|
|
// On the server, the message is marked as erased, but not overwritten.
|
|
func (s *stream) DeleteMsg(ctx context.Context, seq uint64) error {
|
|
return s.deleteMsg(ctx, &msgDeleteRequest{Seq: seq, NoErase: true})
|
|
}
|
|
|
|
// SecureDeleteMsg deletes a message from a stream. The deleted message
|
|
// is overwritten with random data. As a result, this operation is slower
|
|
// than DeleteMsg.
|
|
func (s *stream) SecureDeleteMsg(ctx context.Context, seq uint64) error {
|
|
return s.deleteMsg(ctx, &msgDeleteRequest{Seq: seq})
|
|
}
|
|
|
|
func (s *stream) deleteMsg(ctx context.Context, req *msgDeleteRequest) error {
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
r, err := json.Marshal(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subj := fmt.Sprintf(apiMsgDeleteT, s.name)
|
|
var resp msgDeleteResponse
|
|
if _, err = s.js.apiRequestJSON(ctx, subj, &resp, r); err != nil {
|
|
return err
|
|
}
|
|
if !resp.Success {
|
|
return fmt.Errorf("%w: %s", ErrMsgDeleteUnsuccessful, resp.Error.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListConsumers returns ConsumerInfoLister enabling iterating over a
|
|
// channel of consumer infos.
|
|
func (s *stream) ListConsumers(ctx context.Context) ConsumerInfoLister {
|
|
l := &consumerLister{
|
|
js: s.js,
|
|
consumers: make(chan *ConsumerInfo),
|
|
}
|
|
go func() {
|
|
defer close(l.consumers)
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
for {
|
|
page, err := l.consumerInfos(ctx, s.name)
|
|
if err != nil && !errors.Is(err, ErrEndOfData) {
|
|
l.err = err
|
|
return
|
|
}
|
|
for _, info := range page {
|
|
select {
|
|
case <-ctx.Done():
|
|
l.err = ctx.Err()
|
|
return
|
|
default:
|
|
}
|
|
if info != nil {
|
|
l.consumers <- info
|
|
}
|
|
}
|
|
if errors.Is(err, ErrEndOfData) {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return l
|
|
}
|
|
|
|
func (s *consumerLister) Info() <-chan *ConsumerInfo {
|
|
return s.consumers
|
|
}
|
|
|
|
func (s *consumerLister) Err() error {
|
|
return s.err
|
|
}
|
|
|
|
// ConsumerNames returns a ConsumerNameLister enabling iterating over a
|
|
// channel of consumer names.
|
|
func (s *stream) ConsumerNames(ctx context.Context) ConsumerNameLister {
|
|
l := &consumerLister{
|
|
js: s.js,
|
|
names: make(chan string),
|
|
}
|
|
go func() {
|
|
defer close(l.names)
|
|
ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
for {
|
|
page, err := l.consumerNames(ctx, s.name)
|
|
if err != nil && !errors.Is(err, ErrEndOfData) {
|
|
l.err = err
|
|
return
|
|
}
|
|
for _, info := range page {
|
|
select {
|
|
case l.names <- info:
|
|
case <-ctx.Done():
|
|
l.err = ctx.Err()
|
|
return
|
|
}
|
|
}
|
|
if errors.Is(err, ErrEndOfData) {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return l
|
|
}
|
|
|
|
func (s *consumerLister) Name() <-chan string {
|
|
return s.names
|
|
}
|
|
|
|
// consumerInfos fetches the next ConsumerInfo page
|
|
func (s *consumerLister) consumerInfos(ctx context.Context, stream string) ([]*ConsumerInfo, error) {
|
|
if s.pageInfo != nil && s.offset >= s.pageInfo.Total {
|
|
return nil, ErrEndOfData
|
|
}
|
|
|
|
req, err := json.Marshal(
|
|
apiPagedRequest{Offset: s.offset},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
slSubj := fmt.Sprintf(apiConsumerListT, stream)
|
|
var resp consumerListResponse
|
|
_, err = s.js.apiRequestJSON(ctx, slSubj, &resp, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Error != nil {
|
|
return nil, resp.Error
|
|
}
|
|
|
|
s.pageInfo = &resp.apiPaged
|
|
s.offset += len(resp.Consumers)
|
|
return resp.Consumers, nil
|
|
}
|
|
|
|
// consumerNames fetches the next consumer names page
|
|
func (s *consumerLister) consumerNames(ctx context.Context, stream string) ([]string, error) {
|
|
if s.pageInfo != nil && s.offset >= s.pageInfo.Total {
|
|
return nil, ErrEndOfData
|
|
}
|
|
|
|
req, err := json.Marshal(
|
|
apiPagedRequest{Offset: s.offset},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
slSubj := fmt.Sprintf(apiConsumerNamesT, stream)
|
|
var resp consumerNamesResponse
|
|
_, err = s.js.apiRequestJSON(ctx, slSubj, &resp, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Error != nil {
|
|
return nil, resp.Error
|
|
}
|
|
|
|
s.pageInfo = &resp.apiPaged
|
|
s.offset += len(resp.Consumers)
|
|
return resp.Consumers, nil
|
|
}
|
|
|
|
// UnpinConsumer unpins the currently pinned client for a consumer for the given group name.
|
|
// If consumer does not exist, ErrConsumerNotFound is returned.
|
|
func (s *stream) UnpinConsumer(ctx context.Context, consumer string, group string) error {
|
|
return unpinConsumer(ctx, s.js, s.name, consumer, group)
|
|
}
|