Major rewrite for NATS JetStream API

API made more consistent. Noun followed by verb.
Name arguments in request subejcts are always at the end now.
Remove enabled call, just use account info.
Getting a message directly from a stream is treated like an admin API and requires JSON request.
Deleting a message directly as well.
StreamList and ConsumerList now include details and support paging.
Streams and Consumers now contain a created field in their info.

Signed-off-by: Derek Collison <derek@nats.io>
This commit is contained in:
Derek Collison
2020-05-02 18:28:51 -07:00
parent 17aca11002
commit cadd39a01c
12 changed files with 794 additions and 470 deletions

View File

@@ -910,7 +910,9 @@ func (a *Account) sendTrackingLatency(si *serviceImport, responder *client) bool
m1.merge(m2)
si.acc.mu.Unlock()
a.srv.sendInternalAccountMsg(a, si.latency.subject, m1)
a.mu.Lock()
si.rc = nil
a.mu.Unlock()
return true
}
si.m1 = sl
@@ -918,7 +920,9 @@ func (a *Account) sendTrackingLatency(si *serviceImport, responder *client) bool
return false
} else {
a.srv.sendInternalAccountMsg(a, si.latency.subject, sl)
a.mu.Lock()
si.rc = nil
a.mu.Unlock()
}
return true
}

View File

@@ -3183,7 +3183,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
// For now these will only be on $JS.ACK prefixed reply subjects.
if len(creply) > 0 &&
c.kind != CLIENT && c.kind != SYSTEM && c.kind != JETSTREAM && c.kind != ACCOUNT &&
bytes.HasPrefix(creply, []byte(jetStreamAckPre)) {
bytes.HasPrefix(creply, []byte(jsAckPre)) {
// We need to rewrite the subject and the reply.
if li := bytes.LastIndex(creply, []byte("@")); li != 0 && li < len(creply)-1 {
subj, creply = creply[li+1:], creply[:li]

View File

@@ -33,6 +33,7 @@ import (
type ConsumerInfo struct {
Stream string `json:"stream_name"`
Name string `json:"name"`
Created time.Time `json:"created"`
Config ConsumerConfig `json:"config"`
Delivered SequencePair `json:"delivered"`
AckFloor SequencePair `json:"ack_floor"`
@@ -214,6 +215,7 @@ type Consumer struct {
sfreq int32
ackEventT string
deliveryExcEventT string
created time.Time
}
const (
@@ -230,7 +232,6 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
}
var err error
// For now expect a literal subject if its not empty. Empty means work queue mode (pull mode).
if config.DeliverSubject != _EMPTY_ {
if !subjectIsLiteral(config.DeliverSubject) {
@@ -322,7 +323,7 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
// Hold mset lock here.
mset.mu.Lock()
// If this one is durable and already exists, we let that be ok as long as the configs match=.
// If this one is durable and already exists, we let that be ok as long as the configs match.
if isDurableConsumer(config) {
if eo, ok := mset.consumers[config.Durable]; ok {
mset.mu.Unlock()
@@ -375,13 +376,14 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
// Set name, which will be durable name if set, otherwise we create one at random.
o := &Consumer{mset: mset,
config: *config,
dsubj: config.DeliverSubject,
active: true,
qch: make(chan struct{}),
fch: make(chan struct{}),
sfreq: int32(sampleFreq),
maxdc: uint64(config.MaxDeliver),
config: *config,
dsubj: config.DeliverSubject,
active: true,
qch: make(chan struct{}),
fch: make(chan struct{}),
sfreq: int32(sampleFreq),
maxdc: uint64(config.MaxDeliver),
created: time.Now().UTC(),
}
if isDurableConsumer(config) {
o.name = config.Durable
@@ -396,8 +398,8 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
// already under lock, mset.Name() would deadlock
o.stream = mset.config.Name
o.ackEventT = JetStreamMetricConsumerAckPre + "." + o.stream + "." + o.name
o.deliveryExcEventT = JetStreamAdvisoryConsumerMaxDeliveryExceedPre + "." + o.stream + "." + o.name
o.ackEventT = JSMetricConsumerAckPre + "." + o.stream + "." + o.name
o.deliveryExcEventT = JSAdvisoryConsumerMaxDeliveryExceedPre + "." + o.stream + "." + o.name
store, err := mset.store.ConsumerStore(o.name, config)
if err != nil {
@@ -446,10 +448,11 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
// We will remember the template to generate replies with sequence numbers and use
// that to scanf them back in.
mn := mset.config.Name
pre := fmt.Sprintf(JetStreamAckT, mn, o.name)
pre := fmt.Sprintf(jsAckT, mn, o.name)
o.ackReplyT = fmt.Sprintf("%s.%%d.%%d.%%d.%%d", pre)
ackSubj := fmt.Sprintf("%s.*.*.*.*", pre)
if sub, err := mset.subscribeInternal(ackSubj, o.processAck); err != nil {
mset.mu.Unlock()
return nil, err
} else {
o.ackSub = sub
@@ -457,8 +460,9 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
// Setup the internal sub for next message requests.
if !o.isPushMode() {
o.nextMsgSubj = fmt.Sprintf(JetStreamRequestNextT, mn, o.name)
o.nextMsgSubj = fmt.Sprintf(JSApiRequestNextT, mn, o.name)
if sub, err := mset.subscribeInternal(o.nextMsgSubj, o.processNextMsgReq); err != nil {
mset.mu.Unlock()
o.Delete()
return nil, err
} else {
@@ -494,6 +498,21 @@ func (mset *Stream) AddConsumer(config *ConsumerConfig) (*Consumer, error) {
return o, nil
}
// Created returns created time.
func (o *Consumer) Created() time.Time {
o.mu.Lock()
created := o.created
o.mu.Unlock()
return created
}
// Internal to allow creation time to be restored.
func (o *Consumer) setCreated(created time.Time) {
o.mu.Lock()
o.created = created
o.mu.Unlock()
}
// This will check for extended interest in a subject. If we have local interest we just return
// that, but in the absence of local interest and presence of gateways or service imports we need
// to check those as well.
@@ -716,9 +735,10 @@ func (o *Consumer) updateStateLoop() {
func (o *Consumer) Info() *ConsumerInfo {
o.mu.Lock()
info := &ConsumerInfo{
Stream: o.stream,
Name: o.name,
Config: o.config,
Stream: o.stream,
Name: o.name,
Created: o.created,
Config: o.config,
Delivered: SequencePair{
ConsumerSeq: o.dseq - 1,
StreamSeq: o.sseq - 1,
@@ -1305,6 +1325,7 @@ func (o *Consumer) StreamSeqFromReply(reply string) uint64 {
return seq
}
// Grab encoded information in the reply subject for a delivered message.
func (o *Consumer) ReplyInfo(reply string) (sseq, dseq, dcount uint64, ts int64) {
n, err := fmt.Sscanf(reply, o.ackReplyT, &dcount, &sseq, &dseq, &ts)
if err != nil || n != 4 {

View File

@@ -49,13 +49,25 @@ type FileStoreConfig struct {
SyncInterval time.Duration
}
// FileStreamInfo allows us to remember created time.
type FileStreamInfo struct {
Created time.Time
StreamConfig
}
// File ConsumerInfo is used for creating consumer stores.
type FileConsumerInfo struct {
Created time.Time
ConsumerConfig
}
type fileStore struct {
mu sync.RWMutex
state StreamState
scb func(int64)
ageChk *time.Timer
syncTmr *time.Timer
cfg StreamConfig
cfg FileStreamInfo
fcfg FileStoreConfig
lmb *msgBlock
blks []*msgBlock
@@ -156,6 +168,10 @@ const (
)
func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
return newFileStoreWithCreated(fcfg, cfg, time.Now())
}
func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created time.Time) (*fileStore, error) {
if cfg.Name == "" {
return nil, fmt.Errorf("name required")
}
@@ -192,7 +208,7 @@ func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
fs := &fileStore{
fcfg: fcfg,
cfg: cfg,
cfg: FileStreamInfo{Created: created, StreamConfig: cfg},
wmb: &bytes.Buffer{},
fch: make(chan struct{}),
qch: make(chan struct{}),
@@ -219,9 +235,13 @@ func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
if err := fs.recoverMsgs(); err != nil {
return nil, err
}
// Write our meta data.
if err := fs.writeStreamMeta(); err != nil {
return nil, err
// Write our meta data iff does not exist.
meta := path.Join(fcfg.StoreDir, JetStreamMetaFile)
if _, err := os.Stat(meta); err != nil && os.IsNotExist(err) {
if err := fs.writeStreamMeta(); err != nil {
return nil, err
}
}
go fs.flushLoop(fs.fch, fs.qch)
@@ -244,8 +264,9 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
}
fs.mu.Lock()
new_cfg := FileStreamInfo{Created: fs.cfg.Created, StreamConfig: *cfg}
old_cfg := fs.cfg
fs.cfg = *cfg
fs.cfg = new_cfg
if err := fs.writeStreamMeta(); err != nil {
fs.cfg = old_cfg
fs.mu.Unlock()
@@ -2020,7 +2041,7 @@ func (fs *fileStore) Snapshot() (io.ReadCloser, error) {
type consumerFileStore struct {
mu sync.Mutex
fs *fileStore
cfg *ConsumerConfig
cfg *FileConsumerInfo
name string
odir string
ifn string
@@ -2046,9 +2067,10 @@ func (fs *fileStore) ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerSt
if err := os.MkdirAll(odir, 0755); err != nil {
return nil, fmt.Errorf("could not create consumer directory - %v", err)
}
csi := &FileConsumerInfo{ConsumerConfig: *cfg}
o := &consumerFileStore{
fs: fs,
cfg: cfg,
cfg: csi,
name: name,
odir: odir,
ifn: path.Join(odir, consumerState),
@@ -2062,8 +2084,13 @@ func (fs *fileStore) ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerSt
}
o.hh = hh
if err := o.writeConsumerMeta(); err != nil {
return nil, err
// Write our meta data iff does not exist.
meta := path.Join(odir, JetStreamMetaFile)
if _, err := os.Stat(meta); err != nil && os.IsNotExist(err) {
csi.Created = time.Now().UTC()
if err := o.writeConsumerMeta(); err != nil {
return nil, err
}
}
fs.mu.Lock()

View File

@@ -184,7 +184,7 @@ func (a *Account) enableAllJetStreamServiceImports() error {
}
// In case the enabled import exists here.
a.removeServiceImport(JetStreamEnabled)
a.removeServiceImport(JSApiAccountInfo)
sys := s.SystemAccount()
for _, export := range allJsExports {
@@ -197,7 +197,7 @@ func (a *Account) enableAllJetStreamServiceImports() error {
// enableJetStreamEnabledServiceImportOnly will enable the single service import responder.
// Should we do them all regardless?
func (a *Account) enableJetStreamEnabledServiceImportOnly() error {
func (a *Account) enableJetStreamInfoServiceImportOnly() error {
a.mu.RLock()
s := a.srv
a.mu.RUnlock()
@@ -206,7 +206,7 @@ func (a *Account) enableJetStreamEnabledServiceImportOnly() error {
return fmt.Errorf("jetstream account not registered")
}
sys := s.SystemAccount()
if err := a.AddServiceImport(sys, JetStreamEnabled, _EMPTY_); err != nil {
if err := a.AddServiceImport(sys, JSApiAccountInfo, _EMPTY_); err != nil {
return fmt.Errorf("Error setting up jetstream service imports for account: %v", err)
}
return nil
@@ -254,7 +254,7 @@ func (s *Server) configAllJetStreamAccounts() error {
}
// We will setup basic service imports to respond to
// requests if JS is enabled for this account.
if err := acc.enableJetStreamEnabledServiceImportOnly(); err != nil {
if err := acc.enableJetStreamInfoServiceImportOnly(); err != nil {
return err
}
}
@@ -451,6 +451,7 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error {
}
}
// Now recover the streams.
fis, _ := ioutil.ReadDir(sdir)
for _, fi := range fis {
mdir := path.Join(sdir, fi.Name())
@@ -486,7 +487,7 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error {
continue
}
var cfg StreamConfig
var cfg FileStreamInfo
if err := json.Unmarshal(buf, &cfg); err != nil {
s.Warnf(" Error unmarshalling Stream metafile: %v", err)
continue
@@ -496,11 +497,14 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error {
s.Warnf(" Error adding Stream %q to Template %q: %v", cfg.Name, cfg.Template, err)
}
}
mset, err := a.AddStream(&cfg)
mset, err := a.AddStream(&cfg.StreamConfig)
if err != nil {
s.Warnf(" Error recreating Stream %q: %v", cfg.Name, err)
continue
}
if !cfg.Created.IsZero() {
mset.setCreated(cfg.Created)
}
stats := mset.State()
s.Noticef(" Restored %s messages for Stream %q", comma(int64(stats.Msgs)), fi.Name())
@@ -527,16 +531,20 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error {
s.Warnf(" Missing Consumer checksum for %q", metasum)
continue
}
var cfg ConsumerConfig
var cfg FileConsumerInfo
if err := json.Unmarshal(buf, &cfg); err != nil {
s.Warnf(" Error unmarshalling Consumer metafile: %v", err)
continue
}
obs, err := mset.AddConsumer(&cfg)
obs, err := mset.AddConsumer(&cfg.ConsumerConfig)
if err != nil {
s.Warnf(" Error adding Consumer: %v", err)
continue
}
if !cfg.Created.IsZero() {
obs.setCreated(cfg.Created)
}
if err := obs.readStoredState(); err != nil {
s.Warnf(" Error restoring Consumer state: %v", err)
}

View File

@@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"net"
"sort"
"strconv"
"strings"
"time"
@@ -27,118 +28,121 @@ import (
// Request API subjects for JetStream.
const (
// JetStreamEnabled allows a user to dynamically check if JetStream is enabled for an account.
// Will return +OK on success, otherwise will timeout.
JetStreamEnabled = "$JS.ENABLED"
// JetStreamInfo is for obtaining general information about JetStream for this account.
// JSApiInfo is for obtaining general information about JetStream for this account.
// Will return JSON response.
JetStreamInfo = "$JS.INFO"
JSApiAccountInfo = "$JS.API.INFO"
// JetStreamCreateTemplate is the endpoint to create new stream templates.
// Will return +OK on success and -ERR on failure.
JetStreamCreateTemplate = "$JS.TEMPLATE.*.CREATE"
JetStreamCreateTemplateT = "$JS.TEMPLATE.%s.CREATE"
// JetStreamListTemplates is the endpoint to list all stream templates for this account.
// Will return json list of string on success and -ERR on failure.
JetStreamListTemplates = "$JS.TEMPLATE.LIST"
// JetStreamTemplateInfo is for obtaining general information about a named stream template.
// JSApiCreateTemplate is the endpoint to create new stream templates.
// Will return JSON response.
JetStreamTemplateInfo = "$JS.TEMPLATE.*.INFO"
JetStreamTemplateInfoT = "$JS.TEMPLATE.%s.INFO"
JSApiTemplateCreate = "$JS.API.TEMPLATE.CREATE.*"
JSApiTemplateCreateT = "$JS.API.TEMPLATE.CREATE.%s"
// JetStreamDeleteTemplate is the endpoint to delete stream templates.
// Will return +OK on success and -ERR on failure.
JetStreamDeleteTemplate = "$JS.TEMPLATE.*.DELETE"
JetStreamDeleteTemplateT = "$JS.TEMPLATE.%s.DELETE"
// JetStreamCreateStream is the endpoint to create new streams.
// Will return +OK on success and -ERR on failure.
JetStreamCreateStream = "$JS.STREAM.*.CREATE"
JetStreamCreateStreamT = "$JS.STREAM.%s.CREATE"
// JetStreamUpdateStream is the endpoint to update existing streams.
// Will return +OK on success and -ERR on failure.
JetStreamUpdateStream = "$JS.STREAM.*.UPDATE"
JetStreamUpdateStreamT = "$JS.STREAM.%s.UPDATE"
// JetStreamListStreams is the endpoint to list all streams for this account.
// Will return json list of string on success and -ERR on failure.
JetStreamListStreams = "$JS.STREAM.LIST"
// JetStreamStreamInfo is for obtaining general information about a named stream.
// JSApiListTemplates is the endpoint to list all stream templates for this account.
// Will return JSON response.
JetStreamStreamInfo = "$JS.STREAM.*.INFO"
JetStreamStreamInfoT = "$JS.STREAM.%s.INFO"
JSApiTemplates = "$JS.API.TEMPLATES"
// JetStreamDeleteStream is the endpoint to delete streams.
// Will return +OK on success and -ERR on failure.
JetStreamDeleteStream = "$JS.STREAM.*.DELETE"
JetStreamDeleteStreamT = "$JS.STREAM.%s.DELETE"
// JSApiTemplateInfo is for obtaining general information about a named stream template.
// Will return JSON response.
JSApiTemplateInfo = "$JS.API.TEMPLATE.INFO.*"
JSApiTemplateInfoT = "$JS.API.TEMPLATE.INFO.%s"
// JetStreamPurgeStream is the endpoint to purge streams.
// Will return +OK on success and -ERR on failure.
JetStreamPurgeStream = "$JS.STREAM.*.PURGE"
JetStreamPurgeStreamT = "$JS.STREAM.%s.PURGE"
// JSApiDeleteTemplate is the endpoint to delete stream templates.
// Will return JSON response.
JSApiTemplateDelete = "$JS.API.TEMPLATE.DELETE.*"
JSApiTemplateDeleteT = "$JS.API.TEMPLATE.DELETE.%s"
// JetStreamDeleteMsg is the endpoint to delete messages from a stream.
// Will return +OK on success and -ERR on failure.
JetStreamDeleteMsg = "$JS.STREAM.*.MSG.DELETE"
JetStreamDeleteMsgT = "$JS.STREAM.%s.MSG.DELETE"
// JSApiCreateStream is the endpoint to create new streams.
// Will return JSON response.
JSApiStreamCreate = "$JS.API.STREAM.CREATE.*"
JSApiStreamCreateT = "$JS.API.STREAM.CREATE.%s"
// JetStreamCreateConsumer is the endpoint to create durable consumers for streams.
// JSApiUpdateStream is the endpoint to update existing streams.
// Will return JSON response.
JSApiStreamUpdate = "$JS.API.STREAM.UPDATE.*"
JSApiStreamUpdateT = "$JS.API.STREAM.UPDATE.%s"
// JSApiListStreams is the endpoint to list all streams for this account.
// Will return JSON response.
JSApiStreams = "$JS.API.STREAMS"
// JSApiStreamInfo is for obtaining general information about a named stream.
// Will return JSON response.
JSApiStreamInfo = "$JS.API.STREAM.INFO.*"
JSApiStreamInfoT = "$JS.API.STREAM.INFO.%s"
// JSApiDeleteStream is the endpoint to delete streams.
// Will return JSON response.
JSApiStreamDelete = "$JS.API.STREAM.DELETE.*"
JSApiStreamDeleteT = "$JS.API.STREAM.DELETE.%s"
// JSApiPurgeStream is the endpoint to purge streams.
// Will return JSON response.
JSApiStreamPurge = "$JS.API.STREAM.PURGE.*"
JSApiStreamPurgeT = "$JS.API.STREAM.PURGE.%s"
// JSApiDeleteMsg is the endpoint to delete messages from a stream.
// Will return JSON response.
JSApiMsgDelete = "$JS.API.MSG.DELETE.*"
JSApiMsgDeleteT = "$JS.API.MSG.DELETE.%s"
// JSApiMsgGet is the template for direct requests for a message by its stream sequence number.
// Will return JSON response.
JSApiMsgGet = "$JS.API.MSG.GET.*"
JSApiMsgGetT = "$JS.API.MSG.GET.%s"
// JSApiConsumerCreate is the endpoint to create ephemeral consumers for streams.
// Will return JSON response.
JSApiConsumerCreate = "$JS.API.CONSUMER.CREATE.*"
JSApiConsumerCreateT = "$JS.API.CONSUMER.CREATE.%s"
// JSApiDurableConsumerCreate is the endpoint to create ephemeral consumers for streams.
// You need to include the stream and consumer name in the subject.
// Will return +OK on success and -ERR on failure.
JetStreamCreateConsumer = "$JS.STREAM.*.CONSUMER.*.CREATE"
JetStreamCreateConsumerT = "$JS.STREAM.%s.CONSUMER.%s.CREATE"
JSApiDurableCreate = "$JS.API.DURABLE.CREATE.*.*"
JSApiDurableCreateT = "$JS.API.DURABLE.CREATE.%s.%s"
// JetStreamCreateEphemeralConsumer is the endpoint to create ephemeral consumers for streams.
// Will return +OK <consumer name> on success and -ERR on failure.
JetStreamCreateEphemeralConsumer = "$JS.STREAM.*.EPHEMERAL.CONSUMER.CREATE"
JetStreamCreateEphemeralConsumerT = "$JS.STREAM.%s.EPHEMERAL.CONSUMER.CREATE"
// JetStreamConsumers is the endpoint to list all consumers for the stream.
// Will return json list of string on success and -ERR on failure.
JetStreamConsumers = "$JS.STREAM.*.CONSUMERS"
JetStreamConsumersT = "$JS.STREAM.%s.CONSUMERS"
// JetStreamConsumerInfo is for obtaining general information about a consumer.
// JSApiConsumers is the endpoint to list all consumers for the stream.
// Will return JSON response.
JetStreamConsumerInfo = "$JS.STREAM.*.CONSUMER.*.INFO"
JetStreamConsumerInfoT = "$JS.STREAM.%s.CONSUMER.%s.INFO"
JSApiConsumers = "$JS.API.CONSUMERS.*"
JSApiConsumersT = "$JS.API.CONSUMERS.%s"
// JetStreamDeleteConsumer is the endpoint to delete consumers.
// Will return +OK on success and -ERR on failure.
JetStreamDeleteConsumer = "$JS.STREAM.*.CONSUMER.*.DELETE"
JetStreamDeleteConsumerT = "$JS.STREAM.%s.CONSUMER.%s.DELETE"
// JSApiConsumerInfo is for obtaining general information about a consumer.
// Will return JSON response.
JSApiConsumerInfo = "$JS.API.CONSUMER.INFO.*.*"
JSApiConsumerInfoT = "$JS.API.CONSUMER.INFO.%s.%s"
// JetStreamAckT is the template for the ack message stream coming back from an consumer
// JSApiDeleteConsumer is the endpoint to delete consumers.
// Will return JSON response.
JSApiConsumerDelete = "$JS.API.CONSUMER.DELETE.*.*"
JSApiConsumerDeleteT = "$JS.API.CONSUMER.DELETE.%s.%s"
// JSApiRequestNextT is the prefix for the request next message(s) for a consumer in worker/pull mode.
JSApiRequestNextT = "$JS.API.CONSUMER.MSG.NEXT.%s.%s"
///////////////////////
// FIXME(dlc)
///////////////////////
// JetStreamAckT is the template for the ack message stream coming back from a consumer
// when they ACK/NAK, etc a message.
JetStreamAckT = "$JS.ACK.%s.%s"
jetStreamAckPre = "$JS.ACK."
// FIXME(dlc) - What do we really need here??
jsAckT = "$JS.ACK.%s.%s"
jsAckPre = "$JS.ACK."
// JetStreamRequestNextT is the prefix for the request next message(s) for a consumer in worker/pull mode.
JetStreamRequestNextT = "$JS.STREAM.%s.CONSUMER.%s.NEXT"
// JSAdvisoryPrefix is a prefix for all JetStream advisories.
JSAdvisoryPrefix = "$JS.EVENT.ADVISORY"
// JetStreamMsgBySeqT is the template for direct requests for a message by its stream sequence number.
JetStreamMsgBySeqT = "$JS.STREAM.%s.MSG.BYSEQ"
// JSMetricPrefix is a prefix for all JetStream metrics.
JSMetricPrefix = "$JS.EVENT.METRIC"
// JetStreamAdvisoryPrefix is a prefix for all JetStream advisories.
JetStreamAdvisoryPrefix = "$JS.EVENT.ADVISORY"
// JetStreamMetricPrefix is a prefix for all JetStream metrics.
JetStreamMetricPrefix = "$JS.EVENT.METRIC"
// JetStreamMetricConsumerAckPre is a metric containing ack latency.
JetStreamMetricConsumerAckPre = JetStreamMetricPrefix + ".CONSUMER_ACK"
// JSMetricConsumerAckPre is a metric containing ack latency.
JSMetricConsumerAckPre = "$JS.EVENT.METRIC.CONSUMER.ACK"
// JetStreamAdvisoryConsumerMaxDeliveryExceedPre is a notification published when a message exceeds its delivery threshold.
JetStreamAdvisoryConsumerMaxDeliveryExceedPre = JetStreamAdvisoryPrefix + ".MAX_DELIVERIES"
JSAdvisoryConsumerMaxDeliveryExceedPre = "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES"
// JetStreamAPIAuditAdvisory is a notification about JetStream API access.
JetStreamAPIAuditAdvisory = JetStreamAdvisoryPrefix + ".API"
// FIXME - Add in details about who..
JSAuditAdvisory = "$JS.EVENT.ADVISORY.API"
)
// Responses for API calls.
@@ -150,10 +154,10 @@ type ApiError struct {
Description string `json:"description,omitempty"`
}
// JSEnabledResponse is the response to see if JetStream is enabled for this account.
type JSApiEnabledResponse struct {
Error *ApiError `json:"error,omitempty"`
Enabled bool `json:"enabled"`
// JSApiAccountInfoResponse reports back information on jetstream for this account.
type JSApiAccountInfoResponse struct {
Error *ApiError `json:"error,omitempty"`
*JetStreamAccountStats
}
// JSApiStreamCreateResponse stream creation.
@@ -174,10 +178,22 @@ type JSApiStreamInfoResponse struct {
*StreamInfo
}
// JSApiStreamListResponse list of streams.
type JSApiStreamListResponse struct {
Error *ApiError `json:"error,omitempty"`
Streams []string `json:"streams,omitempty"`
// Maximum entries we will return.
// TODO(dlc) - with header or request support could request chunked response.
const JSApiListLimit = 256
type JSApiStreamsRequest struct {
Offset int `json:"offset"`
}
// JSApiStreamsResponse list of streams.
// A nil request is valid and means all streams.
type JSApiStreamsResponse struct {
Error *ApiError `json:"error,omitempty"`
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Streams []*StreamInfo `json:"streams,omitempty"`
}
// JSApiStreamPurgeResponse.
@@ -204,6 +220,17 @@ type JSApiMsgDeleteResponse struct {
Success bool `json:"success,omitempty"`
}
// JSApiMsgGetRequest get a message request.
type JSApiMsgGetRequest struct {
Seq uint64 `json:"seq"`
}
// JSApiMsgGetResponse.
type JSApiMsgGetResponse struct {
Error *ApiError `json:"error,omitempty"`
Message *StoredMsg `json:"message,omitempty"`
}
// JSApiConsumerCreateResponse.
type JSApiConsumerCreateResponse struct {
Error *ApiError `json:"error,omitempty"`
@@ -222,10 +249,18 @@ type JSApiConsumerInfoResponse struct {
*ConsumerInfo
}
// JSApiConsumerListResponse.
type JSApiConsumerListResponse struct {
Error *ApiError `json:"error,omitempty"`
Consumers []string `json:"streams,omitempty"`
// JSApiConsumersRequest
type JSApiConsumersRequest struct {
Offset int `json:"offset"`
}
// JSApiConsumersResponse.
type JSApiConsumersResponse struct {
Error *ApiError `json:"error,omitempty"`
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Consumers []*ConsumerInfo `json:"streams,omitempty"`
}
// JSApiStreamTemplateCreateResponse for creating templates.
@@ -259,24 +294,24 @@ var (
// For easier handling of exports and imports.
var allJsExports = []string{
JetStreamEnabled,
JetStreamInfo,
JetStreamCreateTemplate,
JetStreamListTemplates,
JetStreamTemplateInfo,
JetStreamDeleteTemplate,
JetStreamCreateStream,
JetStreamUpdateStream,
JetStreamListStreams,
JetStreamStreamInfo,
JetStreamDeleteStream,
JetStreamPurgeStream,
JetStreamDeleteMsg,
JetStreamCreateConsumer,
JetStreamCreateEphemeralConsumer,
JetStreamConsumers,
JetStreamConsumerInfo,
JetStreamDeleteConsumer,
JSApiAccountInfo,
JSApiTemplateCreate,
JSApiTemplates,
JSApiTemplateInfo,
JSApiTemplateDelete,
JSApiStreamCreate,
JSApiStreamUpdate,
JSApiStreams,
JSApiStreamInfo,
JSApiStreamDelete,
JSApiStreamPurge,
JSApiMsgDelete,
JSApiMsgGet,
JSApiConsumerCreate,
JSApiDurableCreate,
JSApiConsumers,
JSApiConsumerInfo,
JSApiConsumerDelete,
}
func (s *Server) setJetStreamExportSubs() error {
@@ -284,24 +319,24 @@ func (s *Server) setJetStreamExportSubs() error {
subject string
handler msgHandler
}{
{JetStreamEnabled, s.isJsEnabledRequest},
{JetStreamInfo, s.jsAccountInfoRequest},
{JetStreamCreateTemplate, s.jsCreateTemplateRequest},
{JetStreamListTemplates, s.jsTemplateListRequest},
{JetStreamTemplateInfo, s.jsTemplateInfoRequest},
{JetStreamDeleteTemplate, s.jsTemplateDeleteRequest},
{JetStreamCreateStream, s.jsCreateStreamRequest},
{JetStreamUpdateStream, s.jsStreamUpdateRequest},
{JetStreamListStreams, s.jsStreamListRequest},
{JetStreamStreamInfo, s.jsStreamInfoRequest},
{JetStreamDeleteStream, s.jsStreamDeleteRequest},
{JetStreamPurgeStream, s.jsStreamPurgeRequest},
{JetStreamDeleteMsg, s.jsMsgDeleteRequest},
{JetStreamCreateConsumer, s.jsCreateConsumerRequest},
{JetStreamCreateEphemeralConsumer, s.jsCreateEphemeralConsumerRequest},
{JetStreamConsumers, s.jsConsumersRequest},
{JetStreamConsumerInfo, s.jsConsumerInfoRequest},
{JetStreamDeleteConsumer, s.jsConsumerDeleteRequest},
{JSApiAccountInfo, s.jsAccountInfoRequest},
{JSApiTemplateCreate, s.jsTemplateCreateRequest},
{JSApiTemplates, s.jsTemplateListRequest},
{JSApiTemplateInfo, s.jsTemplateInfoRequest},
{JSApiTemplateDelete, s.jsTemplateDeleteRequest},
{JSApiStreamCreate, s.jsStreamCreateRequest},
{JSApiStreamUpdate, s.jsStreamUpdateRequest},
{JSApiStreams, s.jsStreamListRequest},
{JSApiStreamInfo, s.jsStreamInfoRequest},
{JSApiStreamDelete, s.jsStreamDeleteRequest},
{JSApiStreamPurge, s.jsStreamPurgeRequest},
{JSApiMsgDelete, s.jsMsgDeleteRequest},
{JSApiMsgGet, s.jsMsgGetRequest},
{JSApiConsumerCreate, s.jsConsumerCreateRequest},
{JSApiDurableCreate, s.jsDurableCreateRequest},
{JSApiConsumers, s.jsConsumerListRequest},
{JSApiConsumerInfo, s.jsConsumerInfoRequest},
{JSApiConsumerDelete, s.jsConsumerDeleteRequest},
}
for _, p := range pairs {
@@ -317,26 +352,12 @@ func (s *Server) sendAPIResponse(c *client, subject, reply, request, response st
s.sendJetStreamAPIAuditAdvisory(c, subject, request, response)
}
// Request to check if jetstream is enabled.
func (s *Server) isJsEnabledRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
b, _ := json.MarshalIndent(&JSApiEnabledResponse{Enabled: c.acc.JetStreamEnabled()}, "", " ")
s.sendAPIResponse(c, subject, reply, string(msg), string(b))
}
type JSApiAccountInfo struct {
Error *ApiError `json:"error,omitempty"`
*JetStreamAccountStats
}
// Request for current usage and limits for this account.
func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
var resp JSApiAccountInfo
var resp JSApiAccountInfoResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
} else {
@@ -350,8 +371,25 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, rep
s.sendAPIResponse(c, subject, reply, string(msg), string(b))
}
// Helpers for token extraction.
func templateNameFromSubject(subject string) string {
return tokenAt(subject, 5)
}
func streamNameFromSubject(subject string) string {
return tokenAt(subject, 5)
}
func consumerNameFromSubject(subject string) string {
return tokenAt(subject, 6)
}
func durableNameFromSubject(subject string) string {
return tokenAt(subject, 6)
}
// Request to create a new template.
func (s *Server) jsCreateTemplateRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
@@ -367,7 +405,7 @@ func (s *Server) jsCreateTemplateRequest(sub *subscription, c *client, subject,
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
templateName := subjectToken(subject, 2)
templateName := templateNameFromSubject(subject)
if templateName != cfg.Name {
resp.Error = &ApiError{Code: 400, Description: "template name in subject does not match request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
@@ -424,7 +462,7 @@ func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, subject, re
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
name := subjectToken(subject, 2)
name := templateNameFromSubject(subject)
t, err := c.acc.LookupStreamTemplate(name)
if err != nil {
resp.Error = jsError(err)
@@ -456,7 +494,7 @@ func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, subject,
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
name := subjectToken(subject, 2)
name := templateNameFromSubject(subject)
err := c.acc.DeleteStreamTemplate(name)
if err != nil {
resp.Error = jsError(err)
@@ -484,10 +522,11 @@ func jsError(err error) *ApiError {
}
// Request to create a stream.
func (s *Server) jsCreateStreamRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
var resp JSApiStreamCreateResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
@@ -500,7 +539,7 @@ func (s *Server) jsCreateStreamRequest(sub *subscription, c *client, subject, re
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
streamName := subjectToken(subject, 2)
streamName := streamNameFromSubject(subject)
if streamName != cfg.Name {
resp.Error = &ApiError{Code: 400, Description: "stream name in subject does not match request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
@@ -513,7 +552,7 @@ func (s *Server) jsCreateStreamRequest(sub *subscription, c *client, subject, re
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
resp.StreamInfo = &StreamInfo{State: mset.State(), Config: mset.Config()}
resp.StreamInfo = &StreamInfo{Created: mset.Created(), State: mset.State(), Config: mset.Config()}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -534,7 +573,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
streamName := subjectToken(subject, 2)
streamName := streamNameFromSubject(subject)
if streamName != cfg.Name {
resp.Error = &ApiError{Code: 400, Description: "stream name in subject does not match request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
@@ -552,7 +591,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re
return
}
resp.StreamInfo = &StreamInfo{State: mset.State(), Config: mset.Config()}
resp.StreamInfo = &StreamInfo{Created: mset.Created(), State: mset.State(), Config: mset.Config()}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -561,16 +600,41 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl
if c == nil || c.acc == nil {
return
}
var resp JSApiStreamListResponse
var resp JSApiStreamsResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
msets := c.acc.Streams()
for _, mset := range msets {
resp.Streams = append(resp.Streams, mset.Name())
var offset int
if !isEmptyRequest(msg) {
var req JSApiStreamsRequest
if err := json.Unmarshal(msg, &req); err != nil {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
offset = req.Offset
}
// TODO(dlc) - Maybe hold these results for large results that we expect to be paged.
// TODO(dlc) - If this list is long maybe do this in a Go routine?
msets := c.acc.Streams()
sort.Slice(msets, func(i, j int) bool {
return strings.Compare(msets[i].config.Name, msets[j].config.Name) < 0
})
for _, mset := range msets[offset:] {
info := &StreamInfo{Created: mset.Created(), State: mset.State(), Config: mset.Config()}
resp.Streams = append(resp.Streams, info)
if len(resp.Streams) >= JSApiListLimit {
break
}
}
resp.Total = len(msets)
resp.Limit = JSApiListLimit
resp.Offset = offset
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -590,14 +654,14 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
name := subjectToken(subject, 2)
name := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(name)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
resp.StreamInfo = &StreamInfo{State: mset.State(), Config: mset.Config()}
resp.StreamInfo = &StreamInfo{Created: mset.Created(), State: mset.State(), Config: mset.Config()}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -636,7 +700,7 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
stream := subjectToken(subject, 2)
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
@@ -676,7 +740,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
return
}
stream := subjectToken(subject, 2)
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
@@ -698,6 +762,52 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
// Request to get a raw stream message.
func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
var resp JSApiMsgGetResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if len(msg) == 0 {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
var req JSApiMsgGetRequest
if err := json.Unmarshal(msg, &req); err != nil {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
subj, msg, ts, err := mset.store.LoadMsg(req.Seq)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
resp.Message = &StoredMsg{
Subject: subj,
Sequence: req.Seq,
Data: msg,
Time: time.Unix(0, ts),
}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
// Request to purge a stream.
func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
@@ -714,7 +824,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
stream := subjectToken(subject, 2)
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
@@ -728,61 +838,20 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
}
// Request to create a durable consumer.
func (s *Server) jsCreateConsumerRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
var resp JSApiConsumerCreateResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
var req CreateConsumerRequest
if err := json.Unmarshal(msg, &req); err != nil {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
streamName := subjectToken(subject, 2)
if streamName != req.Stream {
resp.Error = &ApiError{Code: 400, Description: "stream name in subject does not match request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
stream, err := c.acc.LookupStream(req.Stream)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
// Now check we do not have a durable.
if req.Config.Durable == _EMPTY_ {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but a durable name was not set"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
consumerName := subjectToken(subject, 4)
if consumerName != req.Config.Durable {
resp.Error = &ApiError{Code: 400, Description: "consumer name in subject does not match durable name in request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
o, err := stream.AddConsumer(&req.Config)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
resp.ConsumerInfo = o.Info()
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
func (s *Server) jsDurableCreateRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
s.jsConsumerCreate(sub, c, subject, reply, msg, true)
}
// Request to create an ephemeral consumer.
func (s *Server) jsCreateEphemeralConsumerRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
// Request to create a consumer.
func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
s.jsConsumerCreate(sub, c, subject, reply, msg, false)
}
func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply string, msg []byte, expectDurable bool) {
if c == nil || c.acc == nil {
return
}
var resp JSApiConsumerCreateResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
@@ -795,7 +864,7 @@ func (s *Server) jsCreateEphemeralConsumerRequest(sub *subscription, c *client,
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
streamName := subjectToken(subject, 2)
streamName := streamNameFromSubject(subject)
if streamName != req.Stream {
resp.Error = &ApiError{Code: 400, Description: "stream name in subject does not match request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
@@ -807,11 +876,36 @@ func (s *Server) jsCreateEphemeralConsumerRequest(sub *subscription, c *client,
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
// Now check we do not have a durable.
if req.Config.Durable != _EMPTY_ {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but a durable name was set"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
if expectDurable {
if numTokens(subject) != 6 {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but no durable name set in subject"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
// Now check on requirements for durable request.
if req.Config.Durable == _EMPTY_ {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but a durable name was not set"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
consumerName := durableNameFromSubject(subject)
if consumerName != req.Config.Durable {
resp.Error = &ApiError{Code: 400, Description: "consumer name in subject does not match durable name in request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
} else {
if numTokens(subject) != 5 {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if req.Config.Durable != _EMPTY_ {
resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but a durable name was set in request"}
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
}
o, err := stream.AddConsumer(&req.Config)
@@ -825,32 +919,49 @@ func (s *Server) jsCreateEphemeralConsumerRequest(sub *subscription, c *client,
}
// Request for the list of all consumers.
func (s *Server) jsConsumersRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, reply string, msg []byte) {
if c == nil || c.acc == nil {
return
}
var resp JSApiConsumerListResponse
var resp JSApiConsumersResponse
if !c.acc.JetStreamEnabled() {
resp.Error = jsNotEnabledErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
var offset int
if !isEmptyRequest(msg) {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
var req JSApiConsumersRequest
if err := json.Unmarshal(msg, &req); err != nil {
resp.Error = jsBadRequestErr
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
offset = req.Offset
}
name := subjectToken(subject, 2)
mset, err := c.acc.LookupStream(name)
streamName := tokenAt(subject, 4)
mset, err := c.acc.LookupStream(streamName)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
obs := mset.Consumers()
for _, o := range obs {
resp.Consumers = append(resp.Consumers, o.Name())
sort.Slice(obs, func(i, j int) bool {
return strings.Compare(obs[i].name, obs[j].name) < 0
})
for _, o := range obs[offset:] {
resp.Consumers = append(resp.Consumers, o.Info())
if len(resp.Consumers) >= JSApiListLimit {
break
}
}
resp.Total = len(obs)
resp.Limit = JSApiListLimit
resp.Offset = offset
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -871,14 +982,14 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re
return
}
stream := subjectToken(subject, 2)
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
consumer := subjectToken(subject, 4)
consumer := consumerNameFromSubject(subject)
obs := mset.LookupConsumer(consumer)
if obs == nil {
resp.Error = &ApiError{Code: 400, Description: "consumer not found"}
@@ -905,14 +1016,14 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject,
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
stream := subjectToken(subject, 2)
stream := streamNameFromSubject(subject)
mset, err := c.acc.LookupStream(stream)
if err != nil {
resp.Error = jsError(err)
s.sendAPIResponse(c, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
consumer := subjectToken(subject, 4)
consumer := consumerNameFromSubject(subject)
obs := mset.LookupConsumer(consumer)
if obs == nil {
resp.Error = &ApiError{Code: 400, Description: "consumer not found"}
@@ -989,7 +1100,7 @@ func (s *Server) sendJetStreamAPIAuditAdvisory(c *client, subject, request, resp
ej, err := json.MarshalIndent(e, "", " ")
if err == nil {
s.sendInternalAccountMsg(c.acc, JetStreamAPIAuditAdvisory, ej)
s.sendInternalAccountMsg(c.acc, JSAuditAdvisory, ej)
} else {
s.Warnf("JetStream could not marshal audit event for account %q: %v", c.acc.Name, err)
}

View File

@@ -1083,7 +1083,13 @@ func (s *Server) reloadAuthorization() {
newAcc.sl = acc.sl
newAcc.rm = acc.rm
newAcc.js = acc.js
newAcc.imports.rrMap = acc.imports.rrMap
if len(acc.imports.rrMap) > 0 {
newAcc.imports.rrMap = make(map[string][]*serviceRespEntry)
for k, v := range acc.imports.rrMap {
newAcc.imports.rrMap[k] = v
}
}
acc.mu.RUnlock()
// Check if current and new config of this account are same

View File

@@ -14,7 +14,6 @@
package server
import (
"encoding/json"
"fmt"
"math"
"path"
@@ -43,7 +42,7 @@ type StreamConfig struct {
}
// PubAck is the detail you get back from a publish to a stream that was successful.
// e.g. +OK {"stream": "my_stream", "seq": 22}
// e.g. +OK {"stream": "Orders", "seq": 22}
type PubAck struct {
Stream string `json:"stream"`
Seq uint64 `json:"seq"`
@@ -51,8 +50,9 @@ type PubAck struct {
// StreamInfo shows config and current state for this stream.
type StreamInfo struct {
Config StreamConfig `json:"config"`
State StreamState `json:"state"`
Config StreamConfig `json:"config"`
Created time.Time `json:"created"`
State StreamState `json:"state"`
}
// Stream is a jetstream stream of messages. When we receive a message internally destined
@@ -69,6 +69,7 @@ type Stream struct {
store StreamStore
consumers map[string]*Consumer
config StreamConfig
created time.Time
}
const (
@@ -166,6 +167,21 @@ func (a *Account) AddStreamWithStore(config *StreamConfig, fsConfig *FileStoreCo
return mset, nil
}
// Created returns created time.
func (mset *Stream) Created() time.Time {
mset.mu.RLock()
created := mset.created
mset.mu.RUnlock()
return created
}
// Internal to allow creation time to be restored.
func (mset *Stream) setCreated(created time.Time) {
mset.mu.Lock()
mset.created = created
mset.mu.Unlock()
}
// Check to see if these subjects overlap with existing subjects.
// Lock should be held.
func (jsa *jsAccount) subjectsOverlap(subjects []string) bool {
@@ -377,12 +393,6 @@ func (mset *Stream) subscribeToStream() error {
return err
}
}
// Now subscribe for direct access
subj := fmt.Sprintf(JetStreamMsgBySeqT, mset.config.Name)
if _, err := mset.subscribeInternal(subj, mset.processMsgBySeq); err != nil {
return err
}
return nil
}
@@ -454,10 +464,11 @@ func (mset *Stream) unsubscribe(sub *subscription) {
}
func (mset *Stream) setupStore(fsCfg *FileStoreConfig) error {
mset.mu.Lock()
defer mset.mu.Unlock()
mset.created = time.Now().UTC()
switch mset.config.Storage {
case MemoryStorage:
ms, err := newMemStore(&mset.config)
@@ -466,7 +477,7 @@ func (mset *Stream) setupStore(fsCfg *FileStoreConfig) error {
}
mset.store = ms
case FileStorage:
fs, err := newFileStore(*fsCfg, mset.config)
fs, err := newFileStoreWithCreated(*fsCfg, mset.config, mset.created)
if err != nil {
return err
}
@@ -477,52 +488,6 @@ func (mset *Stream) setupStore(fsCfg *FileStoreConfig) error {
return nil
}
// processMsgBySeq will return the message at the given sequence, or an -ERR if not found.
func (mset *Stream) processMsgBySeq(_ *subscription, _ *client, subject, reply string, msg []byte) {
mset.mu.Lock()
store := mset.store
c := mset.client
name := mset.config.Name
mset.mu.Unlock()
if c == nil {
return
}
var response []byte
var seq uint64
var err error
// If no sequence arg assume last sequence we have.
if len(msg) == 0 {
stats := store.State()
seq = stats.LastSeq
} else {
seq, err = strconv.ParseUint(string(msg), 10, 64)
if err != nil {
c.Debugf("JetStream request for message from message: %q - %q bad sequence arg %q", c.acc.Name, name, msg)
response = []byte("-ERR 'bad sequence argument'")
mset.sendq <- &jsPubMsg{reply, _EMPTY_, _EMPTY_, response, nil, 0}
return
}
}
subj, msg, ts, err := store.LoadMsg(seq)
if err != nil {
c.Debugf("JetStream request for message: %q - %q - %d error %v", c.acc.Name, name, seq, err)
response = []byte("-ERR 'could not load message from storage'")
mset.sendq <- &jsPubMsg{reply, _EMPTY_, _EMPTY_, response, nil, 0}
return
}
sm := &StoredMsg{
Subject: subj,
Sequence: seq,
Data: msg,
Time: time.Unix(0, ts),
}
response, _ = json.MarshalIndent(sm, "", " ")
mset.sendq <- &jsPubMsg{reply, _EMPTY_, _EMPTY_, response, nil, 0}
}
// processInboundJetStreamMsg handles processing messages bound for a stream.
func (mset *Stream) processInboundJetStreamMsg(_ *subscription, _ *client, subject, reply string, msg []byte) {
mset.mu.Lock()

View File

@@ -1128,9 +1128,24 @@ func SubjectsCollide(subj1, subj2 string) bool {
return true
}
// Returns number of tokens in the subject.
func numTokens(subject string) int {
var numTokens int
if len(subject) == 0 {
return 0
}
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
numTokens++
}
}
return numTokens + 1
}
// Fast way to return an indexed token.
func subjectToken(subject string, index uint8) string {
ti, start := uint8(0), 0
// This is one based, so first token is TokenAt(subject, 1)
func tokenAt(subject string, index uint8) string {
ti, start := uint8(1), 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
if ti == index {

View File

@@ -673,11 +673,12 @@ func TestSubjectToken(t *testing.T) {
t.Fatalf("Expected token of %q, got %q", expected, token)
}
}
checkToken(subjectToken("foo.bar.baz.*", 0), "foo")
checkToken(subjectToken("foo.bar.baz.*", 1), "bar")
checkToken(subjectToken("foo.bar.baz.*", 2), "baz")
checkToken(subjectToken("foo.bar.baz.*", 3), "*")
checkToken(subjectToken("foo.bar.baz.*", 4), "")
checkToken(tokenAt("foo.bar.baz.*", 0), "")
checkToken(tokenAt("foo.bar.baz.*", 1), "foo")
checkToken(tokenAt("foo.bar.baz.*", 2), "bar")
checkToken(tokenAt("foo.bar.baz.*", 3), "baz")
checkToken(tokenAt("foo.bar.baz.*", 4), "*")
checkToken(tokenAt("foo.bar.baz.*", 5), "")
}
func TestSublistBadSubjectOnRemove(t *testing.T) {

View File

@@ -306,24 +306,22 @@ func benchOptionsForServiceImports() *server.Options {
func addServiceImports(b *testing.B, s *server.Server) {
// Add a bunch of service exports with wildcards, similar to JS.
var exports = []string{
server.JetStreamEnabled,
server.JetStreamInfo,
server.JetStreamCreateTemplate,
server.JetStreamListTemplates,
server.JetStreamTemplateInfo,
server.JetStreamDeleteTemplate,
server.JetStreamCreateStream,
server.JetStreamUpdateStream,
server.JetStreamListStreams,
server.JetStreamStreamInfo,
server.JetStreamDeleteStream,
server.JetStreamPurgeStream,
server.JetStreamDeleteMsg,
server.JetStreamCreateConsumer,
server.JetStreamCreateEphemeralConsumer,
server.JetStreamConsumers,
server.JetStreamConsumerInfo,
server.JetStreamDeleteConsumer,
server.JSApiAccountInfo,
server.JSApiTemplateCreate,
server.JSApiTemplates,
server.JSApiTemplateInfo,
server.JSApiTemplateDelete,
server.JSApiStreamCreate,
server.JSApiStreamUpdate,
server.JSApiStreams,
server.JSApiStreamInfo,
server.JSApiStreamDelete,
server.JSApiStreamPurge,
server.JSApiMsgDelete,
server.JSApiConsumerCreate,
server.JSApiConsumers,
server.JSApiConsumerInfo,
server.JSApiConsumerDelete,
}
foo, _ := s.LookupAccount("$foo")
bar, _ := s.LookupAccount("$bar")

View File

@@ -390,12 +390,10 @@ func TestJetStreamConsumerWithStartTime(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
msg, err := nc.Request(o.RequestNextMsgSubject(), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
sseq, dseq, _, _ := o.ReplyInfo(msg.Reply)
if dseq != 1 {
t.Fatalf("Expected delivered seq of 1, got %d", dseq)
@@ -448,7 +446,7 @@ func TestJetStreamConsumerWithMultipleStartOptions(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
_, err = nc.Request(fmt.Sprintf(server.JetStreamCreateConsumerT, subj, "d"), req, time.Second)
_, err = nc.Request(fmt.Sprintf(server.JSApiConsumerCreateT, subj), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -653,7 +651,7 @@ func TestJetStreamAddStreamBadSubjects(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, _ := nc.Request(fmt.Sprintf(server.JetStreamCreateStreamT, cfg.Name), req, time.Second)
resp, _ := nc.Request(fmt.Sprintf(server.JSApiStreamCreateT, cfg.Name), req, time.Second)
var scResp server.JSApiStreamCreateResponse
if err := json.Unmarshal(resp.Data, &scResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -664,10 +662,10 @@ func TestJetStreamAddStreamBadSubjects(t *testing.T) {
}
}
expectAPIErr(server.StreamConfig{Name: "S", Subjects: []string{"foo.bar."}})
expectAPIErr(server.StreamConfig{Name: "S", Subjects: []string{".."}})
expectAPIErr(server.StreamConfig{Name: "S", Subjects: []string{".*"}})
expectAPIErr(server.StreamConfig{Name: "S", Subjects: []string{".>"}})
expectAPIErr(server.StreamConfig{Name: "MyStream", Subjects: []string{"foo.bar."}})
expectAPIErr(server.StreamConfig{Name: "MyStream", Subjects: []string{".."}})
expectAPIErr(server.StreamConfig{Name: "MyStream", Subjects: []string{".*"}})
expectAPIErr(server.StreamConfig{Name: "MyStream", Subjects: []string{".>"}})
}
func TestJetStreamAddStreamOverlappingSubjects(t *testing.T) {
@@ -2046,6 +2044,8 @@ func TestJetStreamConsumerMaxDeliveryAndServerRestart(t *testing.T) {
}
defer mset.Delete()
streamCreated := mset.Created()
dsubj := "D.TO"
max := 4
@@ -2058,6 +2058,10 @@ func TestJetStreamConsumerMaxDeliveryAndServerRestart(t *testing.T) {
})
defer o.Delete()
consumerCreated := o.Created()
// For calculation of consumer created times below.
time.Sleep(5 * time.Millisecond)
nc := clientConnectToServer(t, s)
defer nc.Close()
@@ -2140,6 +2144,24 @@ func TestJetStreamConsumerMaxDeliveryAndServerRestart(t *testing.T) {
// Now we should have max times three on our sub.
checkSubPending(max * 3)
// Now do some checks on created timestamps.
mset, err = s.GlobalAccount().LookupStream(mname)
if err != nil {
t.Fatalf("Expected to find a stream for %q", mname)
}
if mset.Created() != streamCreated {
t.Fatalf("Stream creation time not restored, wanted %v, got %v", streamCreated, mset.Created())
}
o = mset.LookupConsumer("TO")
if o == nil {
t.Fatalf("Error looking up consumer: %v", err)
}
// Consumer created times can have a very small skew.
delta := o.Created().Sub(consumerCreated)
if delta > 5*time.Millisecond {
t.Fatalf("Consumer creation time not restored, wanted %v, got %v", consumerCreated, o.Created())
}
}
func TestJetStreamDeleteConsumerAndServerRestart(t *testing.T) {
@@ -2804,8 +2826,8 @@ func TestJetStreamMetadata(t *testing.T) {
name string
mconfig *server.StreamConfig
}{
{"MemoryStore", &server.StreamConfig{Name: "DC", Storage: server.MemoryStorage}},
{"FileStore", &server.StreamConfig{Name: "DC", Storage: server.FileStorage}},
{"MemoryStore", &server.StreamConfig{Name: "DC", Retention: server.WorkQueuePolicy, Storage: server.MemoryStorage}},
{"FileStore", &server.StreamConfig{Name: "DC", Retention: server.WorkQueuePolicy, Storage: server.FileStorage}},
}
for _, c := range cases {
@@ -2846,17 +2868,26 @@ func TestJetStreamMetadata(t *testing.T) {
sseq, dseq, dcount, ts := o.ReplyInfo(m.Reply)
mreq := &server.JSApiMsgGetRequest{Seq: sseq}
req, err := json.Marshal(mreq)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Load the original message from the stream to verify ReplyInfo ts against stored message
smsgj, err := nc.Request(fmt.Sprintf(server.JetStreamMsgBySeqT, c.mconfig.Name), []byte(strconv.Itoa(int(sseq))), time.Second)
smsgj, err := nc.Request(fmt.Sprintf(server.JSApiMsgGetT, c.mconfig.Name), req, time.Second)
if err != nil {
t.Fatalf("Could not retrieve stream message: %v", err)
}
var smsg server.StoredMsg
err = json.Unmarshal(smsgj.Data, &smsg)
var resp server.JSApiMsgGetResponse
err = json.Unmarshal(smsgj.Data, &resp)
if err != nil {
t.Fatalf("Could not parse stream message: %v", err)
}
if resp.Message == nil || resp.Error != nil {
t.Fatalf("Did not receive correct response")
}
smsg := resp.Message
if ts != smsg.Time.UnixNano() {
t.Fatalf("Wrong timestamp in ReplyInfo for msg %d, expected %v got %v", i, ts, smsg.Time.UnixNano())
}
@@ -2869,9 +2900,28 @@ func TestJetStreamMetadata(t *testing.T) {
if dcount != 1 {
t.Fatalf("Expected delivery count to be 1, got %d", dcount)
}
m.Respond(server.AckAck)
}
// Now make sure we get right response when message is missing.
mreq := &server.JSApiMsgGetRequest{Seq: 1}
req, err := json.Marshal(mreq)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Load the original message from the stream to verify ReplyInfo ts against stored message
rmsg, err := nc.Request(fmt.Sprintf(server.JSApiMsgGetT, c.mconfig.Name), req, time.Second)
if err != nil {
t.Fatalf("Could not retrieve stream message: %v", err)
}
var resp server.JSApiMsgGetResponse
err = json.Unmarshal(rmsg.Data, &resp)
if err != nil {
t.Fatalf("Could not parse stream message: %v", err)
}
if resp.Error == nil || resp.Error.Code != 500 || resp.Error.Description != "no message found" {
t.Fatalf("Did not get correct error response: %+v", resp.Error)
}
})
}
}
@@ -4120,22 +4170,12 @@ func TestJetStreamRequestAPI(t *testing.T) {
nc := clientConnectToServer(t, s)
defer nc.Close()
// Should return true
resp, _ := nc.Request(server.JetStreamEnabled, nil, time.Second)
var enabledResp server.JSApiEnabledResponse
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !enabledResp.Enabled {
t.Fatalf("Expected JetStream to be enabled")
}
// This will get the current information about usage and limits for this account.
resp, err := nc.Request(server.JetStreamInfo, nil, time.Second)
resp, err := nc.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var info server.JSApiAccountInfo
var info server.JSApiAccountInfoResponse
if err := json.Unmarshal(resp.Data, &info); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4151,7 +4191,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamCreateStreamT, msetCfg.Name), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiStreamCreateT, msetCfg.Name), req, time.Second)
var scResp server.JSApiStreamCreateResponse
if err := json.Unmarshal(resp.Data, &scResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4159,6 +4199,9 @@ func TestJetStreamRequestAPI(t *testing.T) {
if scResp.StreamInfo == nil || scResp.Error != nil {
t.Fatalf("Did not receive correct response")
}
if time.Since(scResp.Created) > time.Second {
t.Fatalf("Created time seems wrong: %v\n", scResp.Created)
}
checkBadRequest := func(e *server.ApiError, description string) {
t.Helper()
@@ -4175,7 +4218,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Check that the name in config has to match the name in the subject
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamCreateStreamT, "BOB"), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiStreamCreateT, "BOB"), req, time.Second)
scResp.Error, scResp.StreamInfo = nil, nil
if err := json.Unmarshal(resp.Data, &scResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4189,7 +4232,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamUpdateStreamT, msetCfg.Name), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiStreamUpdateT, msetCfg.Name), req, time.Second)
scResp.Error, scResp.StreamInfo = nil, nil
if err := json.Unmarshal(resp.Data, &scResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4199,7 +4242,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Now lookup info again and see that we can see the new stream.
resp, err = nc.Request(server.JetStreamInfo, nil, time.Second)
resp, err = nc.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4211,16 +4254,25 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Make sure list works.
resp, err = nc.Request(server.JetStreamListStreams, nil, time.Second)
var listResponse server.JSApiStreamListResponse
resp, err = nc.Request(server.JSApiStreams, nil, time.Second)
var listResponse server.JSApiStreamsResponse
if err = json.Unmarshal(resp.Data, &listResponse); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(listResponse.Streams) != 1 {
t.Fatalf("Expected only 1 stream but got %d", len(listResponse.Streams))
}
if listResponse.Streams[0] != msetCfg.Name {
t.Fatalf("Expected to get %q, but got %q", msetCfg.Name, listResponse.Streams[0])
if listResponse.Total != 1 {
t.Fatalf("Expected total to be 1 but got %d", listResponse.Total)
}
if listResponse.Offset != 0 {
t.Fatalf("Expected offset to be 0 but got %d", listResponse.Offset)
}
if listResponse.Limit != server.JSApiListLimit {
t.Fatalf("Expected limit to be %d but got %d", server.JSApiListLimit, listResponse.Limit)
}
if listResponse.Streams[0].Config.Name != msetCfg.Name {
t.Fatalf("Expected to get %q, but got %q", msetCfg.Name, listResponse.Streams[0].Config.Name)
}
// Now send some messages, then we can poll for info on this stream.
@@ -4229,7 +4281,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
nc.Request("foo", []byte("WELCOME JETSTREAM"), time.Second)
}
resp, err = nc.Request(fmt.Sprintf(server.JetStreamStreamInfoT, msetCfg.Name), nil, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiStreamInfoT, msetCfg.Name), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4240,9 +4292,12 @@ func TestJetStreamRequestAPI(t *testing.T) {
if msi.State.Msgs != uint64(toSend) {
t.Fatalf("Expected to get %d msgs, got %d", toSend, msi.State.Msgs)
}
if time.Since(msi.Created) > time.Second {
t.Fatalf("Created time seems wrong: %v\n", msi.Created)
}
// Looking up one that is not there should yield an error.
resp, err = nc.Request(fmt.Sprintf(server.JetStreamStreamInfoT, "BOB"), nil, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiStreamInfoT, "BOB"), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4262,7 +4317,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateEphemeralConsumerT, msetCfg.Name), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumerCreateT, msetCfg.Name), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4276,7 +4331,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
sub, _ := nc.SubscribeSync(delivery)
nc.Flush()
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateEphemeralConsumerT, msetCfg.Name), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumerCreateT, msetCfg.Name), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4287,6 +4342,9 @@ func TestJetStreamRequestAPI(t *testing.T) {
if ccResp.ConsumerInfo == nil || ccResp.Error != nil {
t.Fatalf("Got a bad response %+v", ccResp)
}
if time.Since(ccResp.Created) > time.Second {
t.Fatalf("Created time seems wrong: %v\n", ccResp.Created)
}
checkFor(t, 250*time.Millisecond, 10*time.Millisecond, func() error {
if nmsgs, _, _ := sub.Pending(); err != nil || nmsgs != toSend {
@@ -4296,7 +4354,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
})
// Check that we get an error if the stream name in the subject does not match the config.
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateEphemeralConsumerT, "BOB"), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumerCreateT, "BOB"), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4307,12 +4365,12 @@ func TestJetStreamRequestAPI(t *testing.T) {
// Since we do not have interest this should have failed.
checkBadRequest(ccResp.Error, "stream name in subject does not match request")
// Get the list of all of the obervables for our stream.
resp, err = nc.Request(fmt.Sprintf(server.JetStreamConsumersT, msetCfg.Name), nil, time.Second)
// Get the list of all of the consumers for our stream.
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumersT, msetCfg.Name), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var clResponse server.JSApiConsumerListResponse
var clResponse server.JSApiConsumersResponse
if err = json.Unmarshal(resp.Data, &clResponse); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4320,8 +4378,8 @@ func TestJetStreamRequestAPI(t *testing.T) {
t.Fatalf("Expected only 1 consumer but got %d", len(clResponse.Consumers))
}
// Now let's get info about our consumer.
cName := clResponse.Consumers[0]
resp, err = nc.Request(fmt.Sprintf(server.JetStreamConsumerInfoT, msetCfg.Name, cName), nil, time.Second)
cName := clResponse.Consumers[0].Name
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumerInfoT, msetCfg.Name, cName), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4350,7 +4408,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Now delete the consumer.
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamDeleteConsumerT, msetCfg.Name, cName), nil, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiConsumerDeleteT, msetCfg.Name, cName), nil, time.Second)
var cdResp server.JSApiConsumerDeleteResponse
if err = json.Unmarshal(resp.Data, &cdResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4368,7 +4426,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateEphemeralConsumerT, msetCfg.Name), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiConsumerCreateT, msetCfg.Name), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4376,10 +4434,10 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
checkBadRequest(ccResp.Error, "consumer expected to be ephemeral but a durable name was set")
checkBadRequest(ccResp.Error, "consumer expected to be ephemeral but a durable name was set in request")
// Now make sure we can create a durable on the subject with the proper name.
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateConsumerT, msetCfg.Name, obsReq.Config.Durable), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiDurableCreateT, msetCfg.Name, obsReq.Config.Durable), req, time.Second)
ccResp.Error, ccResp.ConsumerInfo = nil, nil
if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4397,7 +4455,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateConsumerT, msetCfg.Name, obsReq.Config.Durable), req2, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiDurableCreateT, msetCfg.Name, obsReq.Config.Durable), req2, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4407,24 +4465,13 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
checkBadRequest(ccResp.Error, "consumer expected to be durable but a durable name was not set")
// Now make sure we can't fake the consumer name.
resp, err = nc.Request(fmt.Sprintf(server.JetStreamCreateConsumerT, msetCfg.Name, "WRONG_CONSUMER_NAME"), req, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
ccResp.Error, ccResp.ConsumerInfo = nil, nil
if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
checkBadRequest(ccResp.Error, "consumer name in subject does not match durable name in request")
// Now delete a msg.
dreq := server.JSApiMsgDeleteRequest{Seq: 2}
dreqj, err := json.Marshal(dreq)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamDeleteMsgT, msetCfg.Name), dreqj, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiMsgDeleteT, msetCfg.Name), dreqj, time.Second)
var delMsgResp server.JSApiMsgDeleteResponse
if err = json.Unmarshal(resp.Data, &delMsgResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4434,7 +4481,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Now purge the stream.
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamPurgeStreamT, msetCfg.Name), nil, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiStreamPurgeT, msetCfg.Name), nil, time.Second)
var pResp server.JSApiStreamPurgeResponse
if err = json.Unmarshal(resp.Data, &pResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4447,7 +4494,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Now delete the stream.
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamDeleteStreamT, msetCfg.Name), nil, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiStreamDeleteT, msetCfg.Name), nil, time.Second)
var dResp server.JSApiStreamDeleteResponse
if err = json.Unmarshal(resp.Data, &dResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4458,7 +4505,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
// Now grab stats again.
// This will get the current information about usage and limits for this account.
resp, err = nc.Request(server.JetStreamInfo, nil, time.Second)
resp, err = nc.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4489,14 +4536,14 @@ func TestJetStreamRequestAPI(t *testing.T) {
}
// Check that the name in config has to match the name in the subject
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamCreateTemplateT, "BOB"), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiTemplateCreateT, "BOB"), req, time.Second)
var stResp server.JSApiStreamTemplateCreateResponse
if err = json.Unmarshal(resp.Data, &stResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
checkBadRequest(stResp.Error, "template name in subject does not match request")
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamCreateTemplateT, template.Name), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiTemplateCreateT, template.Name), req, time.Second)
stResp.Error, stResp.StreamTemplateInfo = nil, nil
if err = json.Unmarshal(resp.Data, &stResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4511,10 +4558,10 @@ func TestJetStreamRequestAPI(t *testing.T) {
req, err = json.Marshal(template)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
t.Fatalf("Unexpected error: %s", err)
}
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamCreateTemplateT, template.Name), req, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiTemplateCreateT, template.Name), req, time.Second)
stResp.Error, stResp.StreamTemplateInfo = nil, nil
if err = json.Unmarshal(resp.Data, &stResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4525,7 +4572,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
// Now grab the list of templates
var tListResp server.JSApiStreamTemplateListResponse
resp, err = nc.Request(server.JetStreamListTemplates, nil, time.Second)
resp, err = nc.Request(server.JSApiTemplates, nil, time.Second)
if err = json.Unmarshal(resp.Data, &tListResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4542,14 +4589,14 @@ func TestJetStreamRequestAPI(t *testing.T) {
// Now delete one.
// Test bad name.
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamDeleteTemplateT, "bob"), nil, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiTemplateDeleteT, "bob"), nil, time.Second)
var tDeleteResp server.JSApiStreamTemplateDeleteResponse
if err = json.Unmarshal(resp.Data, &tDeleteResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
checkServerError(tDeleteResp.Error, "template not found")
resp, _ = nc.Request(fmt.Sprintf(server.JetStreamDeleteTemplateT, "ss"), nil, time.Second)
resp, _ = nc.Request(fmt.Sprintf(server.JSApiTemplateDeleteT, "ss"), nil, time.Second)
tDeleteResp.Error = nil
if err = json.Unmarshal(resp.Data, &tDeleteResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4558,7 +4605,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
t.Fatalf("Did not receive correct response")
}
resp, err = nc.Request(server.JetStreamListTemplates, nil, time.Second)
resp, err = nc.Request(server.JSApiTemplates, nil, time.Second)
tListResp.Error, tListResp.Templates = nil, nil
if err = json.Unmarshal(resp.Data, &tListResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4573,7 +4620,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
// First create a stream from the template
sendStreamMsg(t, nc, "kv.22", "derek")
// Last do info
resp, err = nc.Request(fmt.Sprintf(server.JetStreamTemplateInfoT, "kv"), nil, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiTemplateInfoT, "kv"), nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -4596,7 +4643,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
if len(arg) > 0 {
req = []byte(arg)
}
resp, err = nc.Request(fmt.Sprintf(server.JetStreamDeleteStreamT, "foo_bar_baz"), req, time.Second)
resp, err = nc.Request(fmt.Sprintf(server.JSApiStreamDeleteT, "foo_bar_baz"), req, time.Second)
var dResp server.JSApiStreamDeleteResponse
if err = json.Unmarshal(resp.Data, &dResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
@@ -4612,6 +4659,144 @@ func TestJetStreamRequestAPI(t *testing.T) {
checkEmptyReqArg(" { } ")
}
func TestJetStreamAPIStreamListPaging(t *testing.T) {
s := RunBasicJetStreamServer()
defer s.Shutdown()
// Forced cleanup of all persisted state.
config := s.JetStreamConfig()
if config == nil {
t.Fatalf("Expected non-nil config")
}
defer os.RemoveAll(config.StoreDir)
// Create 4X limit
streamsNum := 4 * server.JSApiListLimit
for i := 1; i <= streamsNum; i++ {
name := fmt.Sprintf("STREAM-%06d", i)
cfg := server.StreamConfig{Name: name}
_, err := s.GlobalAccount().AddStream(&cfg)
if err != nil {
t.Fatalf("Unexpected error adding stream: %v", err)
}
}
// Client for API requests.
nc := clientConnectToServer(t, s)
defer nc.Close()
reqList := func(offset int) []byte {
t.Helper()
var req []byte
if offset > 0 {
req, _ = json.Marshal(&server.JSApiStreamsRequest{Offset: offset})
}
resp, err := nc.Request(server.JSApiStreams, req, time.Second)
if err != nil {
t.Fatalf("Unexpected error getting stream list: %v", err)
}
return resp.Data
}
checkResp := func(resp []byte, expectedLen, expectedOffset int) {
t.Helper()
var listResponse server.JSApiStreamsResponse
if err := json.Unmarshal(resp, &listResponse); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(listResponse.Streams) != expectedLen {
t.Fatalf("Expected only %d streams but got %d", expectedLen, len(listResponse.Streams))
}
if listResponse.Total != streamsNum {
t.Fatalf("Expected total to be %d but got %d", streamsNum, listResponse.Total)
}
if listResponse.Offset != expectedOffset {
t.Fatalf("Expected offset to be %d but got %d", expectedOffset, listResponse.Offset)
}
if expectedLen < 1 {
return
}
// Make sure we get the right stream.
sname := fmt.Sprintf("STREAM-%06d", expectedOffset+1)
if listResponse.Streams[0].Config.Name != sname {
t.Fatalf("Expected stream %q to be first, got %q", sname, listResponse.Streams[0].Config.Name)
}
}
checkResp(reqList(0), server.JSApiListLimit, 0)
checkResp(reqList(server.JSApiListLimit), server.JSApiListLimit, server.JSApiListLimit)
checkResp(reqList(2*server.JSApiListLimit), server.JSApiListLimit, 2*server.JSApiListLimit)
checkResp(reqList(streamsNum), 0, streamsNum)
checkResp(reqList(streamsNum-22), 22, streamsNum-22)
}
func TestJetStreamAPIConsumerListPaging(t *testing.T) {
s := RunBasicJetStreamServer()
defer s.Shutdown()
// Forced cleanup of all persisted state.
config := s.JetStreamConfig()
if config == nil {
t.Fatalf("Expected non-nil config")
}
defer os.RemoveAll(config.StoreDir)
sname := "MYSTREAM"
mset, err := s.GlobalAccount().AddStream(&server.StreamConfig{Name: sname})
if err != nil {
t.Fatalf("Unexpected error adding stream: %v", err)
}
// Client for API requests.
nc := clientConnectToServer(t, s)
defer nc.Close()
sub, _ := nc.SubscribeSync("d.*")
defer sub.Unsubscribe()
nc.Flush()
consumersNum := 4 * server.JSApiListLimit
for i := 1; i <= consumersNum; i++ {
_, err := mset.AddConsumer(&server.ConsumerConfig{DeliverSubject: fmt.Sprintf("d.%d", i)})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
reqListSubject := fmt.Sprintf(server.JSApiConsumersT, sname)
reqList := func(offset int) []byte {
t.Helper()
var req []byte
if offset > 0 {
req, _ = json.Marshal(&server.JSApiConsumersRequest{Offset: offset})
}
resp, err := nc.Request(reqListSubject, req, time.Second)
if err != nil {
t.Fatalf("Unexpected error getting stream list: %v", err)
}
return resp.Data
}
checkResp := func(resp []byte, expectedLen, expectedOffset int) {
t.Helper()
var listResponse server.JSApiConsumersResponse
if err := json.Unmarshal(resp, &listResponse); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(listResponse.Consumers) != expectedLen {
t.Fatalf("Expected only %d streams but got %d", expectedLen, len(listResponse.Consumers))
}
if listResponse.Total != consumersNum {
t.Fatalf("Expected total to be %d but got %d", consumersNum, listResponse.Total)
}
if listResponse.Offset != expectedOffset {
t.Fatalf("Expected offset to be %d but got %d", expectedOffset, listResponse.Offset)
}
}
checkResp(reqList(0), server.JSApiListLimit, 0)
}
func TestJetStreamUpdateStream(t *testing.T) {
cases := []struct {
name string
@@ -5376,31 +5561,14 @@ func TestJetStreamMultipleAccountsBasics(t *testing.T) {
nca := clientConnectToServerWithUP(t, opts, "ua", "pwd")
defer nca.Close()
resp, _ := nca.Request(server.JetStreamEnabled, nil, 250*time.Millisecond)
var enabledResp server.JSApiEnabledResponse
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !enabledResp.Enabled {
t.Fatalf("Expected JetStream to be enabled")
}
ncb := clientConnectToServerWithUP(t, opts, "ub", "pwd")
defer ncb.Close()
resp, _ = ncb.Request(server.JetStreamEnabled, nil, 250*time.Millisecond)
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !enabledResp.Enabled {
t.Fatalf("Expected JetStream to be enabled")
}
resp, err := ncb.Request(server.JetStreamInfo, nil, time.Second)
resp, err := ncb.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var info server.JetStreamAccountStats
var info server.JSApiAccountInfoResponse
if err := json.Unmarshal(resp.Data, &info); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -5430,17 +5598,17 @@ func TestJetStreamMultipleAccountsBasics(t *testing.T) {
if resp == nil {
t.Fatalf("No response, possible timeout?")
}
var enabledResp server.JSApiEnabledResponse
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
var iResp server.JSApiAccountInfoResponse
if err := json.Unmarshal(resp.Data, &iResp); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if enabledResp.Enabled {
t.Fatalf("Expected to get a response indicating jetstream is not enabled for this account, got %q", resp.Data)
if iResp.Error == nil {
t.Fatalf("Expected an error on not enabled account")
}
}
// Check C is not enabled. We expect a negative response, not a timeout.
expectNotEnabled(ncc.Request(server.JetStreamEnabled, nil, 250*time.Millisecond))
expectNotEnabled(ncc.Request(server.JSApiAccountInfo, nil, 250*time.Millisecond))
// Now do simple reload and check that we do the right thing. Testing enable and disable and also change in limits
newConf := []byte(`
@@ -5467,27 +5635,27 @@ func TestJetStreamMultipleAccountsBasics(t *testing.T) {
if err := s.Reload(); err != nil {
t.Fatalf("Error on server reload: %v", err)
}
expectNotEnabled(nca.Request(server.JetStreamEnabled, nil, 250*time.Millisecond))
expectNotEnabled(nca.Request(server.JSApiAccountInfo, nil, 250*time.Millisecond))
resp, _ = ncb.Request(server.JetStreamEnabled, nil, 250*time.Millisecond)
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
resp, _ = ncb.Request(server.JSApiAccountInfo, nil, 250*time.Millisecond)
if err := json.Unmarshal(resp.Data, &info); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !enabledResp.Enabled {
t.Fatalf("Expected JetStream to be enabled")
if info.Error != nil {
t.Fatalf("Expected JetStream to be enabled, got %+v", info.Error)
}
resp, _ = ncc.Request(server.JetStreamEnabled, nil, 250*time.Millisecond)
if err := json.Unmarshal(resp.Data, &enabledResp); err != nil {
resp, _ = ncc.Request(server.JSApiAccountInfo, nil, 250*time.Millisecond)
if err := json.Unmarshal(resp.Data, &info); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !enabledResp.Enabled {
t.Fatalf("Expected JetStream to be enabled")
if info.Error != nil {
t.Fatalf("Expected JetStream to be enabled, got %+v", info.Error)
}
// Now check that limits have been updated.
// Account B
resp, err = ncb.Request(server.JetStreamInfo, nil, time.Second)
resp, err = ncb.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@@ -5509,7 +5677,7 @@ func TestJetStreamMultipleAccountsBasics(t *testing.T) {
}
// Account C
resp, err = ncc.Request(server.JetStreamInfo, nil, time.Second)
resp, err = ncc.Request(server.JSApiAccountInfo, nil, time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}