diff --git a/Gopkg.lock b/Gopkg.lock
index 49efc47c..7a4cd913 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -2,39 +2,30 @@
[[projects]]
- digest = "1:8e379117c74df8f21127a82e1599de345efad7770fa91dd2ea20195471014fb4"
name = "github.com/FZambia/sentinel"
packages = ["."]
- pruneopts = ""
revision = "e69a8dc549bb0aabdc63fbfd9e8bfe971a368153"
version = "v1.0.0"
[[projects]]
branch = "master"
- digest = "1:c0bec5f9b98d0bc872ff5e834fac186b807b656683bd29cb82fb207a1513fabb"
name = "github.com/beorn7/perks"
packages = ["quantile"]
- pruneopts = ""
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
- digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
- pruneopts = ""
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
- digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
- pruneopts = ""
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
- digest = "1:0a3f6a0c68ab8f3d455f8892295503b179e571b7fefe47cc6c556405d1f83411"
name = "github.com/gogo/protobuf"
packages = [
"gogoproto",
@@ -42,135 +33,92 @@
"proto",
"protoc-gen-gogo/descriptor",
"sortkeys",
- "types",
+ "types"
]
- pruneopts = ""
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[projects]]
- digest = "1:f958a1c137db276e52f0b50efee41a1a389dcdded59a69711f3e872757dab34b"
name = "github.com/golang/protobuf"
packages = ["proto"]
- pruneopts = ""
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
- digest = "1:dcf8316121302735c0ac84e05f4686e3b34e284444435e9a206da48d8be18cb1"
name = "github.com/gomodule/redigo"
packages = [
"internal",
- "redis",
+ "redis"
]
- pruneopts = ""
revision = "9c11da706d9b7902c6da69c592f75637793fe121"
version = "v2.0.0"
[[projects]]
- digest = "1:64d212c703a2b94054be0ce470303286b177ad260b2f89a307e3d1bb6c073ef6"
name = "github.com/gorilla/websocket"
packages = ["."]
- pruneopts = ""
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
branch = "master"
- digest = "1:4e43a9d89cc03a5bf460989e699d086a75ba4264c66e5d32ada18b4e83445f44"
name = "github.com/igm/sockjs-go"
packages = ["sockjs"]
- pruneopts = ""
revision = "4e63e74d3787fb81d6568e1d9640bf869ca5ddf7"
[[projects]]
- digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
- pruneopts = ""
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
- digest = "1:be61e8224b84064109eaba8157cbb4bbe6ca12443e182b6624fdfa1c0dcf53d9"
- name = "github.com/nats-io/nuid"
- packages = ["."]
- pruneopts = ""
- revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
- version = "v1.0.0"
-
-[[projects]]
- digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
- pruneopts = ""
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
- digest = "1:4142d94383572e74b42352273652c62afec5b23f325222ed09198f46009022d1"
name = "github.com/prometheus/client_golang"
packages = ["prometheus"]
- pruneopts = ""
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
- digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef"
name = "github.com/prometheus/client_model"
packages = ["go"]
- pruneopts = ""
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
- digest = "1:20f52ee91ee69bd30fb8692aaaa10d6894888dd68c3cec66a7be7e3030f95265"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
- "model",
+ "model"
]
- pruneopts = ""
revision = "38c53a9f4bfcd932d1b00bfc65e256a7fba6b37a"
[[projects]]
branch = "master"
- digest = "1:9015def9b01c5b9f28606fcbe9efcb76657fc20ef1c3102e89d290bff8a6380d"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
- "xfs",
+ "xfs"
]
- pruneopts = ""
revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea"
[[projects]]
- digest = "1:a30066593578732a356dc7e5d7f78d69184ca65aeeff5939241a3ab10559bb06"
name = "github.com/stretchr/testify"
packages = ["assert"]
- pruneopts = ""
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- input-imports = [
- "github.com/FZambia/sentinel",
- "github.com/dgrijalva/jwt-go",
- "github.com/gogo/protobuf/gogoproto",
- "github.com/gogo/protobuf/jsonpb",
- "github.com/gogo/protobuf/proto",
- "github.com/gomodule/redigo/redis",
- "github.com/gorilla/websocket",
- "github.com/igm/sockjs-go/sockjs",
- "github.com/nats-io/nuid",
- "github.com/prometheus/client_golang/prometheus",
- "github.com/stretchr/testify/assert",
- ]
+ inputs-digest = "91f62ab481fb9d1d570ce984473e55460ff0691dd390dc2d7cee6a6c7b88fe36"
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index e95e3572..6751d87e 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -42,10 +42,6 @@ ignored = ["github.com/centrifugal/centrifuge/examples*"]
name = "github.com/igm/sockjs-go"
branch = "master"
-[[constraint]]
- name = "github.com/nats-io/nuid"
- version = "1.0.0"
-
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
diff --git a/client.go b/client.go
index 2ce5ca53..4153cd49 100644
--- a/client.go
+++ b/client.go
@@ -4,6 +4,8 @@ import (
"context"
"encoding/base64"
"fmt"
+ "io"
+ "sort"
"strconv"
"strings"
"sync"
@@ -126,9 +128,20 @@ type Client struct {
presenceTimer *time.Timer
disconnect *Disconnect
- recovery uint32
eventHub *ClientEventHub
+
+ // The following fields help us to synchronize PUB/SUB and history messages during
+ // publication recovery process. At moment we use the fact that subscription requests
+ // processed by client in sequence so only keep a reference to one channel that is
+ // in subscribe process. If we want to process subscriptions in parallel in future
+ // then we have to use separate flags and buffers for each channel here - i.e. maybe
+ // use map.
+ inSubscribe uint32
+ inSubscribeChMu sync.RWMutex
+ inSubscribeCh string
+ pubBufferMu sync.Mutex
+ pubBuffer []*Publication
}
// newClient initializes new Client.
@@ -143,6 +156,7 @@ func newClient(ctx context.Context, n *Node, t transport) (*Client, error) {
node: n,
transport: t,
eventHub: &ClientEventHub{},
+ pubBuffer: make([]*Publication, 0),
}
config := n.Config()
@@ -412,6 +426,28 @@ func (c *Client) close(disconnect *Disconnect) error {
return nil
}
+func (c *Client) isInSubscribe(ch string) bool {
+ if atomic.LoadUint32(&c.inSubscribe) == 1 {
+ c.inSubscribeChMu.RLock()
+ channelMatch := c.inSubscribeCh == ch
+ c.inSubscribeChMu.RUnlock()
+ return channelMatch
+ }
+ return false
+}
+
+func (c *Client) setInSubscribe(ch string, flag bool) {
+ c.inSubscribeChMu.Lock()
+ if flag {
+ atomic.StoreUint32(&c.inSubscribe, 1)
+ c.inSubscribeCh = ch
+ } else {
+ atomic.StoreUint32(&c.inSubscribe, 0)
+ c.inSubscribeCh = ""
+ }
+ c.inSubscribeChMu.Unlock()
+}
+
// Lock must be held outside.
func (c *Client) clientInfo(ch string) *proto.ClientInfo {
var channelInfo proto.Raw
@@ -427,84 +463,163 @@ func (c *Client) clientInfo(ch string) *proto.ClientInfo {
}
}
-// handle dispatches Command into correct command handler.
-func (c *Client) handle(command *proto.Command) (*proto.Reply, *Disconnect) {
+// common data handling logic for Websocket and Sockjs handlers.
+func (c *Client) handleRawData(data []byte, writer *writer) bool {
+ if len(data) == 0 {
+ c.node.logger.log(newLogEntry(LogLevelError, "empty client request received", map[string]interface{}{"client": c.ID(), "user": c.UserID()}))
+ c.close(DisconnectBadRequest)
+ return false
+ }
+
+ enc := c.transport.Encoding()
+
+ encoder := proto.GetReplyEncoder(enc)
+ decoder := proto.GetCommandDecoder(enc, data)
+
+ for {
+ cmd, err := decoder.Decode()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding command", map[string]interface{}{"data": string(data), "client": c.ID(), "user": c.UserID(), "error": err.Error()}))
+ c.close(DisconnectBadRequest)
+ proto.PutCommandDecoder(enc, decoder)
+ proto.PutReplyEncoder(enc, encoder)
+ return false
+ }
+ var encodeErr error
+ write := func(rep *proto.Reply) error {
+ encodeErr = encoder.Encode(rep)
+ if encodeErr != nil {
+ c.node.logger.log(newLogEntry(LogLevelError, "error encoding reply", map[string]interface{}{"reply": fmt.Sprintf("%v", rep), "command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "error": encodeErr.Error()}))
+ }
+ return encodeErr
+ }
+ flush := func() error {
+ buf := encoder.Finish()
+ if len(buf) > 0 {
+ disconnect := writer.write(buf)
+ if disconnect != nil {
+ if c.node.logger.enabled(LogLevelDebug) {
+ c.node.logger.log(newLogEntry(LogLevelDebug, "disconnect after sending reply", map[string]interface{}{"client": c.ID(), "user": c.UserID(), "reason": disconnect.Reason}))
+ }
+ proto.PutCommandDecoder(enc, decoder)
+ proto.PutReplyEncoder(enc, encoder)
+ c.close(disconnect)
+ return fmt.Errorf("flush error")
+ }
+ }
+ encoder.Reset()
+ return nil
+ }
+ disconnect := c.handle(cmd, write, flush)
+ if disconnect != nil {
+ c.node.logger.log(newLogEntry(LogLevelInfo, "disconnect after handling command", map[string]interface{}{"command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "reason": disconnect.Reason}))
+ c.close(disconnect)
+ proto.PutCommandDecoder(enc, decoder)
+ proto.PutReplyEncoder(enc, encoder)
+ return false
+ }
+ if encodeErr != nil {
+ c.close(DisconnectServerError)
+ return false
+ }
+ }
+ buf := encoder.Finish()
+ if len(buf) > 0 {
+ disconnect := writer.write(buf)
+ if disconnect != nil {
+ if c.node.logger.enabled(LogLevelDebug) {
+ c.node.logger.log(newLogEntry(LogLevelDebug, "disconnect after sending reply", map[string]interface{}{"client": c.ID(), "user": c.UserID(), "reason": disconnect.Reason}))
+ }
+ c.close(disconnect)
+ proto.PutCommandDecoder(enc, decoder)
+ proto.PutReplyEncoder(enc, encoder)
+ return false
+ }
+ }
+
+ proto.PutCommandDecoder(enc, decoder)
+ proto.PutReplyEncoder(enc, encoder)
+
+ return true
+}
+
+type replyWriter struct {
+ write func(*proto.Reply) error
+ flush func() error
+}
+
+// handle dispatches Command into correct command handler.
+func (c *Client) handle(cmd *proto.Command, writeFn func(*proto.Reply) error, flush func() error) *Disconnect {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
- return nil, nil
+ return nil
}
c.mu.Unlock()
- var replyRes []byte
- var replyErr *proto.Error
var disconnect *Disconnect
- method := command.Method
- params := command.Params
-
- if command.ID == 0 && method != proto.MethodTypeSend {
- c.node.logger.log(newLogEntry(LogLevelInfo, "command ID required for commands with reply expected", map[string]interface{}{"client": c.ID(), "user": c.UserID()}))
- replyErr = ErrorBadRequest
- } else if method != proto.MethodTypeConnect && !c.authenticated {
- // Client must send connect command first.
- replyErr = ErrorUnauthorized
- } else {
- started := time.Now()
- switch method {
- case proto.MethodTypeConnect:
- replyRes, replyErr, disconnect = c.handleConnect(params)
- case proto.MethodTypeRefresh:
- replyRes, replyErr, disconnect = c.handleRefresh(params)
- case proto.MethodTypeSubscribe:
- replyRes, replyErr, disconnect = c.handleSubscribe(params)
- case proto.MethodTypeSubRefresh:
- replyRes, replyErr, disconnect = c.handleSubRefresh(params)
- case proto.MethodTypeUnsubscribe:
- replyRes, replyErr, disconnect = c.handleUnsubscribe(params)
- case proto.MethodTypePublish:
- replyRes, replyErr, disconnect = c.handlePublish(params)
- case proto.MethodTypePresence:
- replyRes, replyErr, disconnect = c.handlePresence(params)
- case proto.MethodTypePresenceStats:
- replyRes, replyErr, disconnect = c.handlePresenceStats(params)
- case proto.MethodTypeHistory:
- replyRes, replyErr, disconnect = c.handleHistory(params)
- case proto.MethodTypePing:
- replyRes, replyErr, disconnect = c.handlePing(params)
- case proto.MethodTypeRPC:
- replyRes, replyErr, disconnect = c.handleRPC(params)
- case proto.MethodTypeSend:
- disconnect = c.handleSend(params)
- default:
- replyRes, replyErr = nil, ErrorMethodNotFound
- }
- commandDurationSummary.WithLabelValues(strings.ToLower(proto.MethodType_name[int32(method)])).Observe(time.Since(started).Seconds())
- }
+ method := cmd.Method
+ params := cmd.Params
- if disconnect != nil {
- return nil, disconnect
+ write := func(rep *proto.Reply) error {
+ rep.ID = cmd.ID
+ if rep.Error != nil {
+ c.node.logger.log(newLogEntry(LogLevelInfo, "error in reply", map[string]interface{}{"reply": fmt.Sprintf("%v", rep), "command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "error": rep.Error.Error()}))
+ replyErrorCount.WithLabelValues(strings.ToLower(proto.MethodType_name[int32(method)]), strconv.FormatUint(uint64(rep.Error.Code), 10)).Inc()
+ }
+ return writeFn(rep)
}
- if command.ID == 0 {
- // Asynchronous message from client - no need to reply.
- return nil, nil
- }
+ rw := &replyWriter{write, flush}
- if replyErr != nil {
- replyErrorCount.WithLabelValues(strings.ToLower(proto.MethodType_name[int32(method)]), strconv.FormatUint(uint64(replyErr.Code), 10)).Inc()
+ if cmd.ID == 0 && method != proto.MethodTypeSend {
+ c.node.logger.log(newLogEntry(LogLevelInfo, "command ID required for commands with reply expected", map[string]interface{}{"client": c.ID(), "user": c.UserID()}))
+ rw.write(&proto.Reply{Error: ErrorBadRequest})
+ return nil
}
- rep := &proto.Reply{
- ID: command.ID,
- Error: replyErr,
- }
- if replyRes != nil {
- rep.Result = replyRes
+ if method != proto.MethodTypeConnect && !c.authenticated {
+ // Client must send connect command first.
+ rw.write(&proto.Reply{Error: ErrorUnauthorized})
+ return nil
}
- return rep, nil
+ started := time.Now()
+ switch method {
+ case proto.MethodTypeConnect:
+ disconnect = c.handleConnect(params, rw)
+ case proto.MethodTypeRefresh:
+ disconnect = c.handleRefresh(params, rw)
+ case proto.MethodTypeSubscribe:
+ disconnect = c.handleSubscribe(params, rw)
+ case proto.MethodTypeSubRefresh:
+ disconnect = c.handleSubRefresh(params, rw)
+ case proto.MethodTypeUnsubscribe:
+ disconnect = c.handleUnsubscribe(params, rw)
+ case proto.MethodTypePublish:
+ disconnect = c.handlePublish(params, rw)
+ case proto.MethodTypePresence:
+ disconnect = c.handlePresence(params, rw)
+ case proto.MethodTypePresenceStats:
+ disconnect = c.handlePresenceStats(params, rw)
+ case proto.MethodTypeHistory:
+ disconnect = c.handleHistory(params, rw)
+ case proto.MethodTypePing:
+ disconnect = c.handlePing(params, rw)
+ case proto.MethodTypeRPC:
+ disconnect = c.handleRPC(params, rw)
+ case proto.MethodTypeSend:
+ disconnect = c.handleSend(params, rw)
+ default:
+ rw.write(&proto.Reply{Error: ErrorMethodNotFound})
+ }
+ commandDurationSummary.WithLabelValues(strings.ToLower(proto.MethodType_name[int32(method)])).Observe(time.Since(started).Seconds())
+ return disconnect
}
func (c *Client) expire() {
@@ -562,261 +677,265 @@ func (c *Client) expire() {
c.close(DisconnectExpired)
}
-func (c *Client) handleConnect(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleConnect(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeConnect(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding connect", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.connectCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeConnectResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding connect", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handleRefresh(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleRefresh(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeRefresh(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding refresh", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.refreshCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeRefreshResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding refresh", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handleSubscribe(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleSubscribe(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeSubscribe(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding subscribe", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
- }
- resp, disconnect := c.subscribeCmd(cmd)
- if disconnect != nil {
- return nil, nil, disconnect
- }
- if resp.Error != nil {
- return nil, resp.Error, nil
- }
- var replyRes []byte
- if resp.Result != nil {
- replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeSubscribeResult(resp.Result)
- if err != nil {
- c.node.logger.log(newLogEntry(LogLevelError, "error encoding subscribe", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
- }
+ return DisconnectBadRequest
}
- return replyRes, nil, nil
+ return c.subscribeCmd(cmd, rw)
}
-func (c *Client) handleSubRefresh(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleSubRefresh(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeSubRefresh(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding sub refresh", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.subRefreshCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeSubRefreshResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding sub refresh", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handleUnsubscribe(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleUnsubscribe(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeUnsubscribe(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding unsubscribe", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.unsubscribeCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeUnsubscribeResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding unsubscribe", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handlePublish(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handlePublish(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodePublish(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding publish", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.publishCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodePublishResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding publish", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handlePresence(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handlePresence(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodePresence(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding presence", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.presenceCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodePresenceResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding presence", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handlePresenceStats(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handlePresenceStats(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodePresenceStats(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding presence stats", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.presenceStatsCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodePresenceStatsResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding presence stats", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handleHistory(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleHistory(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeHistory(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding history", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.historyCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeHistoryResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding history", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handlePing(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handlePing(params proto.Raw, rw *replyWriter) *Disconnect {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodePing(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding ping", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
resp, disconnect := c.pingCmd(cmd)
if disconnect != nil {
- return nil, nil, disconnect
+ return disconnect
}
if resp.Error != nil {
- return nil, resp.Error, nil
+ rw.write(&proto.Reply{Error: resp.Error})
+ return nil
}
var replyRes []byte
if resp.Result != nil {
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodePingResult(resp.Result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding ping", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
-func (c *Client) handleRPC(params proto.Raw) (proto.Raw, *proto.Error, *Disconnect) {
+func (c *Client) handleRPC(params proto.Raw, rw *replyWriter) *Disconnect {
if c.eventHub.rpcHandler != nil {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeRPC(params)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "error decoding rpc", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
rpcReply := c.eventHub.rpcHandler(RPCEvent{
Data: cmd.Data,
})
if rpcReply.Disconnect != nil {
- return nil, nil, rpcReply.Disconnect
+ return rpcReply.Disconnect
}
if rpcReply.Error != nil {
- return nil, rpcReply.Error, nil
+ rw.write(&proto.Reply{Error: rpcReply.Error})
+ return nil
}
result := &proto.RPCResult{
@@ -827,14 +946,16 @@ func (c *Client) handleRPC(params proto.Raw) (proto.Raw, *proto.Error, *Disconne
replyRes, err = proto.GetResultEncoder(c.transport.Encoding()).EncodeRPCResult(result)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error encoding rpc", map[string]interface{}{"error": err.Error()}))
- return nil, nil, DisconnectServerError
+ return DisconnectServerError
}
- return replyRes, nil, nil
+ rw.write(&proto.Reply{Result: replyRes})
+ return nil
}
- return nil, ErrorNotAvailable, nil
+ rw.write(&proto.Reply{Error: ErrorNotAvailable})
+ return nil
}
-func (c *Client) handleSend(params proto.Raw) *Disconnect {
+func (c *Client) handleSend(params proto.Raw, rw *replyWriter) *Disconnect {
if c.eventHub.messageHandler != nil {
cmd, err := proto.GetParamsDecoder(c.transport.Encoding()).DecodeSend(params)
if err != nil {
@@ -1010,7 +1131,6 @@ func (c *Client) connectCmd(cmd *proto.ConnectRequest) (*proto.ConnectResponse,
Version: version,
Expires: expires,
TTL: ttl,
- Time: uint32(time.Now().Unix()), // TODO: int bounds check?
}
resp.Result = res
@@ -1167,16 +1287,14 @@ func (c *Client) refreshCmd(cmd *proto.RefreshRequest) (*proto.RefreshResponse,
// on channel, if channel if private then we must validate provided sign here before
// actually subscribe client on channel. Optionally we can send missed messages to
// client if it provided last message id seen in channel.
-func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResponse, *Disconnect) {
+func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest, rw *replyWriter) *Disconnect {
channel := cmd.Channel
if channel == "" {
c.node.logger.log(newLogEntry(LogLevelInfo, "channel required for subscribe", map[string]interface{}{"user": c.user, "client": c.uid}))
- return nil, DisconnectBadRequest
+ return DisconnectBadRequest
}
- resp := &proto.SubscribeResponse{}
-
config := c.node.Config()
secret := config.Secret
@@ -1188,8 +1306,8 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
if channelMaxLength > 0 && len(channel) > channelMaxLength {
c.node.logger.log(newLogEntry(LogLevelInfo, "channel too long", map[string]interface{}{"max": channelMaxLength, "channel": channel, "user": c.user, "client": c.uid}))
- resp.Error = ErrorLimitExceeded
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorLimitExceeded})
+ return nil
}
c.mu.RLock()
@@ -1198,8 +1316,8 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
if channelLimit > 0 && numChannels >= channelLimit {
c.node.logger.log(newLogEntry(LogLevelInfo, "maximum limit of channels per client reached", map[string]interface{}{"limit": channelLimit, "user": c.user, "client": c.uid}))
- resp.Error = ErrorLimitExceeded
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorLimitExceeded})
+ return nil
}
c.mu.RLock()
@@ -1208,26 +1326,26 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
if ok {
c.node.logger.log(newLogEntry(LogLevelInfo, "client already subscribed on channel", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid}))
- resp.Error = ErrorAlreadySubscribed
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorAlreadySubscribed})
+ return nil
}
if !c.node.userAllowed(channel, c.user) {
c.node.logger.log(newLogEntry(LogLevelInfo, "user is not allowed to subscribe on channel", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid}))
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
chOpts, ok := c.node.ChannelOpts(channel)
if !ok {
- resp.Error = ErrorNamespaceNotFound
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorNamespaceNotFound})
+ return nil
}
if !chOpts.Anonymous && c.user == "" && !insecure {
c.node.logger.log(newLogEntry(LogLevelInfo, "anonymous user is not allowed to subscribe on channel", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid}))
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
var channelInfo proto.Raw
@@ -1253,8 +1371,8 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
})
if parsedToken == nil && err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "invalid subscription token", map[string]interface{}{"error": err.Error(), "client": c.uid, "user": c.UserID()}))
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
if claims, ok := parsedToken.Claims.(*subscribeTokenClaims); ok && parsedToken.Valid {
tokenChannel = claims.Channel
@@ -1264,12 +1382,12 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
expireAt = claims.StandardClaims.ExpiresAt
if c.uid != tokenClient {
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
if cmd.Channel != tokenChannel {
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
if len(tokenInfo) > 0 {
@@ -1280,7 +1398,7 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
byteInfo, err := base64.StdEncoding.DecodeString(tokenB64info)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelInfo, "can not decode provided info from base64", map[string]interface{}{"user": c.UserID(), "client": c.uid, "error": err.Error()}))
- return resp, DisconnectBadRequest
+ return DisconnectBadRequest
}
channelInfo = byteInfo
}
@@ -1288,16 +1406,16 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
if validationErr, ok := err.(*jwt.ValidationError); ok {
if validationErr.Errors == jwt.ValidationErrorExpired {
// The only problem with token is its expiration - no other errors set in bitfield.
- resp.Error = ErrorTokenExpired
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorTokenExpired})
+ return nil
}
c.node.logger.log(newLogEntry(LogLevelInfo, "invalid subscription token", map[string]interface{}{"error": err.Error(), "client": c.uid, "user": c.UserID()}))
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
c.node.logger.log(newLogEntry(LogLevelInfo, "invalid subscription token", map[string]interface{}{"error": err.Error(), "client": c.uid, "user": c.UserID()}))
- resp.Error = ErrorPermissionDenied
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorPermissionDenied})
+ return nil
}
}
@@ -1306,11 +1424,11 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
Channel: channel,
})
if reply.Disconnect != nil {
- return resp, reply.Disconnect
+ return reply.Disconnect
}
if reply.Error != nil {
- resp.Error = reply.Error
- return resp, nil
+ rw.write(&proto.Reply{Error: reply.Error})
+ return nil
}
if len(reply.ChannelInfo) > 0 && !isPrivateChannel {
channelInfo = reply.ChannelInfo
@@ -1324,8 +1442,8 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
now := time.Now().Unix()
if expireAt < now {
c.node.logger.log(newLogEntry(LogLevelInfo, "subscription expiration must be greater than now", map[string]interface{}{"client": c.uid, "user": c.UserID()}))
- resp.Error = ErrorExpired
- return resp, nil
+ rw.write(&proto.Reply{Error: ErrorExpired})
+ return nil
}
if isPrivateChannel {
// Only expose expiration info to client in private channel case.
@@ -1344,10 +1462,17 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
c.channels[channel] = channelContext
c.mu.Unlock()
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, true)
+ }
+
err := c.node.addSubscription(channel, c)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error adding subscription", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
- return nil, DisconnectServerError
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, false)
+ }
+ return DisconnectServerError
}
c.mu.RLock()
@@ -1358,59 +1483,94 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
err = c.node.addPresence(channel, c.uid, info)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error adding presence", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
- return nil, DisconnectServerError
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, false)
+ }
+ return DisconnectServerError
}
}
+ var pubBufferLocked bool
+
if chOpts.HistoryRecover {
- res.Time = uint32(time.Now().Unix())
- atomic.StoreUint32(&c.recovery, 1)
+ res.Recoverable = true
+
+ var currentSeq uint32
+ var currentGen uint32
+ var currentEpoch string
if cmd.Recover {
// Client provided subscribe request with recover flag on. Try to recover missed
- // publications automatically from history (we suppose here that history configured wisely)
- // based on provided last publication uid seen by client.
- if cmd.Last == "" {
- // Client wants to recover publications but it seems that there were no
- // messages in channel history before, so looks like client missed all
- // existing messages. We can only guarantee that state was successfully
- // recovered if client passed a since value that fits HistoryLifetime
- // interval and history contains less messages than HistorySize.
- publications, err := c.node.History(channel)
- if err != nil {
- c.node.logger.log(newLogEntry(LogLevelError, "error recovering", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
- res.Publications = nil
- res.Recovered = false
- } else {
- res.Publications = publications
- res.Recovered = time.Now().Unix()-int64(cmd.Since)+int64(1) < int64(chOpts.HistoryLifetime) && len(publications) < chOpts.HistorySize
- }
- } else {
- publications, found, err := c.node.recoverHistory(channel, cmd.Last)
- if err != nil {
- c.node.logger.log(newLogEntry(LogLevelError, "error recovering", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
- res.Publications = nil
- res.Recovered = false
- } else {
- res.Publications = publications
- res.Recovered = found || (time.Now().Unix()-int64(cmd.Since)+int64(1) < int64(chOpts.HistoryLifetime) && len(publications) < chOpts.HistorySize)
+ // publications automatically from history (we suppose here that history configured wisely).
+ publications, recovered, recovery, err := c.node.recoverHistory(channel, recovery{cmd.Seq, cmd.Gen, cmd.Epoch})
+ if err != nil {
+ c.node.logger.log(newLogEntry(LogLevelError, "error on recover", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, false)
}
+ return DisconnectServerError
}
+
+ currentSeq = recovery.Seq
+ currentGen = recovery.Gen
+ currentEpoch = recovery.Epoch
+
+ res.Publications = publications
+ res.Recovered = recovered
+
recoveredLabel := "no"
if res.Recovered {
recoveredLabel = "yes"
}
recoverCount.WithLabelValues(recoveredLabel).Inc()
} else {
- // Client don't want to recover messages yet (fresh connect), we just return last
- // publication uid here so it could recover later.
- lastPubUID, err := c.node.lastPublicationUID(channel)
+ recovery, err := c.node.currentRecoveryData(channel)
if err != nil {
c.node.logger.log(newLogEntry(LogLevelError, "error getting last publication ID for channel", map[string]interface{}{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()}))
- return nil, DisconnectServerError
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, false)
+ }
+ return DisconnectServerError
}
- res.Last = lastPubUID
+ currentSeq = recovery.Seq
+ currentGen = recovery.Gen
+ currentEpoch = recovery.Epoch
}
+
+ res.Epoch = currentEpoch
+ res.Seq = currentSeq
+ res.Gen = currentGen
+
+ c.pubBufferMu.Lock()
+ pubBufferLocked = true
+ if len(c.pubBuffer) > 0 {
+ res.Publications = append(res.Publications, c.pubBuffer...)
+ c.pubBuffer = nil
+ }
+ sort.Slice(res.Publications, func(i, j int) bool {
+ if res.Publications[i].Gen != res.Publications[j].Gen {
+ return res.Publications[i].Gen > res.Publications[j].Gen
+ }
+ return res.Publications[i].Seq > res.Publications[j].Seq
+ })
+ res.Publications = uniquePublications(res.Publications)
+ }
+
+ replyRes, err := proto.GetResultEncoder(c.transport.Encoding()).EncodeSubscribeResult(res)
+ if err != nil {
+ c.node.logger.log(newLogEntry(LogLevelError, "error encoding subscribe", map[string]interface{}{"error": err.Error()}))
+ if chOpts.HistoryRecover {
+ c.setInSubscribe(channel, false)
+ }
+ return DisconnectServerError
+ }
+ rw.write(&proto.Reply{Result: replyRes})
+ if chOpts.HistoryRecover {
+ rw.flush()
+ c.setInSubscribe(channel, false)
+ }
+ if pubBufferLocked {
+ c.pubBufferMu.Unlock()
}
if chOpts.JoinLeave {
@@ -1419,8 +1579,47 @@ func (c *Client) subscribeCmd(cmd *proto.SubscribeRequest) (*proto.SubscribeResp
}
go c.node.publishJoin(channel, join, &chOpts)
}
- resp.Result = res
- return resp, nil
+
+ return nil
+}
+
+func (c *Client) writePublication(ch string, pub *Publication, reply *preparedReply) error {
+ if c.isInSubscribe(ch) {
+ // Client currently in process of subscribing to this channel. In this case we keep
+ // publications in slice buffer. Publications from this temporary buffer will be sent in
+ // subscribe reply.
+ c.pubBufferMu.Lock()
+ if c.isInSubscribe(ch) {
+ c.pubBuffer = append(c.pubBuffer, pub)
+ } else {
+ c.pubBufferMu.Unlock()
+ return c.transport.Send(reply)
+ }
+ c.pubBufferMu.Unlock()
+ return nil
+ }
+ return c.transport.Send(reply)
+}
+
+func (c *Client) writeJoin(ch string, reply *preparedReply) error {
+ return c.transport.Send(reply)
+}
+
+func (c *Client) writeLeave(ch string, reply *preparedReply) error {
+ return c.transport.Send(reply)
+}
+
+func uniquePublications(s []*Publication) []*Publication {
+ keys := make(map[uint64]struct{})
+ list := []*Publication{}
+ for _, entry := range s {
+ val := (uint64(entry.Seq))<<32 | uint64(entry.Gen)
+ if _, value := keys[val]; !value {
+ keys[val] = struct{}{}
+ list = append(list, entry)
+ }
+ }
+ return list
}
func (c *Client) subRefreshCmd(cmd *proto.SubRefreshRequest) (*proto.SubRefreshResponse, *Disconnect) {
@@ -1825,13 +2024,5 @@ func (c *Client) historyCmd(cmd *proto.HistoryRequest) (*proto.HistoryResponse,
// pingCmd handles ping command from client.
func (c *Client) pingCmd(cmd *proto.PingRequest) (*proto.PingResponse, *Disconnect) {
- resp := &proto.PingResponse{}
- if atomic.LoadUint32(&c.recovery) == 1 {
- // only pass server time to client in case it has at least one subscription
- // to channel with recovery enabled.
- resp.Result = &proto.PingResult{
- Time: uint32(time.Now().Unix()),
- }
- }
- return resp, nil
+ return &proto.PingResponse{}, nil
}
diff --git a/client_test.go b/client_test.go
index c894a44a..e6172250 100644
--- a/client_test.go
+++ b/client_test.go
@@ -2,6 +2,7 @@ package centrifuge
import (
"context"
+ "encoding/json"
"strconv"
"testing"
"time"
@@ -35,6 +36,18 @@ func getSubscribeToken(channel string, client string, exp int64) string {
return t
}
+func testReplyWriter(replies *[]*proto.Reply) *replyWriter {
+ return &replyWriter{
+ write: func(rep *proto.Reply) error {
+ *replies = append(*replies, rep)
+ return nil
+ },
+ flush: func() error {
+ return nil
+ },
+ }
+}
+
func TestClientEventHub(t *testing.T) {
h := ClientEventHub{}
handler := func(e DisconnectEvent) DisconnectReply {
@@ -270,13 +283,25 @@ func connectClient(t *testing.T, client *Client) *proto.ConnectResult {
return connectResp.Result
}
+func extractSubscribeResult(replies []*proto.Reply) *proto.SubscribeResult {
+ var res proto.SubscribeResult
+ err := json.Unmarshal(replies[0].Result, &res)
+ if err != nil {
+ panic(err)
+ }
+ return &res
+}
+
func subscribeClient(t *testing.T, client *Client, ch string) *proto.SubscribeResult {
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: ch,
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Nil(t, subscribeResp.Error)
- return subscribeResp.Result
+ assert.Nil(t, replies[0].Error)
+ return extractSubscribeResult(replies)
}
func TestClientSubscribe(t *testing.T) {
@@ -290,29 +315,35 @@ func TestClientSubscribe(t *testing.T) {
assert.Equal(t, 0, len(client.Channels()))
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: "test1",
- })
- assert.Nil(t, disconnect)
- assert.Nil(t, subscribeResp.Error)
- assert.Empty(t, subscribeResp.Result.Last)
- assert.False(t, subscribeResp.Result.Recovered)
- assert.Empty(t, subscribeResp.Result.Publications)
+ }, rw)
+ assert.Nil(t, disconnect)
+ assert.Equal(t, 1, len(replies))
+ assert.Nil(t, replies[0].Error)
+ res := extractSubscribeResult(replies)
+ assert.Empty(t, res.Seq)
+ assert.False(t, res.Recovered)
+ assert.Empty(t, res.Publications)
assert.Equal(t, 1, len(client.Channels()))
- subscribeResp, disconnect = client.subscribeCmd(&proto.SubscribeRequest{
+ replies = nil
+ disconnect = client.subscribeCmd(&proto.SubscribeRequest{
Channel: "test2",
- })
+ }, rw)
assert.Equal(t, 2, len(client.Channels()))
-
assert.Equal(t, 1, node.Hub().NumClients())
assert.Equal(t, 2, node.Hub().NumChannels())
- subscribeResp, disconnect = client.subscribeCmd(&proto.SubscribeRequest{
+ replies = nil
+ disconnect = client.subscribeCmd(&proto.SubscribeRequest{
Channel: "test2",
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Equal(t, ErrorAlreadySubscribed, subscribeResp.Error)
+ assert.Equal(t, ErrorAlreadySubscribed, replies[0].Error)
}
func TestClientSubscribePrivateChannelNoToken(t *testing.T) {
@@ -324,11 +355,14 @@ func TestClientSubscribePrivateChannelNoToken(t *testing.T) {
connectClient(t, client)
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Equal(t, ErrorPermissionDenied, subscribeResp.Error)
+ assert.Equal(t, ErrorPermissionDenied, replies[0].Error)
}
func TestClientSubscribePrivateChannelWithToken(t *testing.T) {
@@ -345,26 +379,31 @@ func TestClientSubscribePrivateChannelWithToken(t *testing.T) {
connectClient(t, client)
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
Token: getSubscribeToken("$wrong_channel", "wrong client", 0),
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Equal(t, ErrorPermissionDenied, subscribeResp.Error)
+ assert.Equal(t, ErrorPermissionDenied, replies[0].Error)
- subscribeResp, disconnect = client.subscribeCmd(&proto.SubscribeRequest{
+ replies = nil
+ disconnect = client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
Token: getSubscribeToken("$wrong_channel", client.ID(), 0),
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Equal(t, ErrorPermissionDenied, subscribeResp.Error)
+ assert.Equal(t, ErrorPermissionDenied, replies[0].Error)
- subscribeResp, disconnect = client.subscribeCmd(&proto.SubscribeRequest{
+ replies = nil
+ disconnect = client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
Token: getSubscribeToken("$test1", client.ID(), 0),
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Nil(t, subscribeResp.Error)
+ assert.Nil(t, replies[0].Error)
}
func TestClientSubscribePrivateChannelWithExpiringToken(t *testing.T) {
@@ -381,21 +420,26 @@ func TestClientSubscribePrivateChannelWithExpiringToken(t *testing.T) {
connectClient(t, client)
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
Token: getSubscribeToken("$test1", client.ID(), 10),
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Equal(t, ErrorTokenExpired, subscribeResp.Error)
+ assert.Equal(t, ErrorTokenExpired, replies[0].Error)
- subscribeResp, disconnect = client.subscribeCmd(&proto.SubscribeRequest{
+ replies = nil
+ disconnect = client.subscribeCmd(&proto.SubscribeRequest{
Channel: "$test1",
Token: getSubscribeToken("$test1", client.ID(), time.Now().Unix()+10),
- })
+ }, rw)
assert.Nil(t, disconnect)
- assert.Nil(t, subscribeResp.Error, "token is valid and not expired yet")
- assert.True(t, subscribeResp.Result.Expires, "expires flag must be set")
- assert.True(t, subscribeResp.Result.TTL > 0, "positive TTL must be set")
+ assert.Nil(t, replies[0].Error, "token is valid and not expired yet")
+ res := extractSubscribeResult(replies)
+ assert.True(t, res.Expires, "expires flag must be set")
+ assert.True(t, res.TTL > 0, "positive TTL must be set")
}
func TestClientSubscribeLast(t *testing.T) {
@@ -410,7 +454,12 @@ func TestClientSubscribeLast(t *testing.T) {
transport := newTestTransport()
ctx := context.Background()
newCtx := SetCredentials(ctx, &Credentials{UserID: "42"})
+
client, _ := newClient(newCtx, node, transport)
+ connectClient(t, client)
+
+ result := subscribeClient(t, client, "test")
+ assert.Equal(t, uint32(0), result.Seq)
for i := 0; i < 10; i++ {
node.Publish("test", &Publication{
@@ -419,9 +468,10 @@ func TestClientSubscribeLast(t *testing.T) {
})
}
+ client, _ = newClient(newCtx, node, transport)
connectClient(t, client)
- result := subscribeClient(t, client, "test")
- assert.Equal(t, "9", result.Last)
+ result = subscribeClient(t, client, "test")
+ assert.Equal(t, uint32(10), result.Seq)
}
var recoverTests = []struct {
@@ -429,18 +479,20 @@ var recoverTests = []struct {
HistorySize int
HistoryLifetime int
NumPublications int
- Last string
- Since uint32
+ SinceSeq uint32
NumRecovered int
+ Sleep int
Recovered bool
}{
- {"from_last_uid", 10, 60, 10, "7", 0, 2, true},
- {"empty_last_uid_full_history", 10, 60, 10, "", uint32(time.Now().Unix()), 10, false},
- {"empty_last_uid_short_disconnect", 10, 60, 9, "", uint32(time.Now().Unix()) - 19, 9, true},
- {"empty_last_uid_long_disconnect", 10, 60, 9, "", uint32(time.Now().Unix()) - 59, 9, false},
+ {"from_position", 10, 60, 10, 8, 2, 0, true},
+ {"from_position_that_is_too_far", 10, 60, 20, 8, 10, 0, false},
+ {"same_position_no_history_expected", 10, 60, 7, 7, 0, 0, true},
+ {"empty_position_recover_expected", 10, 60, 4, 0, 4, 0, true},
+ {"from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, false},
+ {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, true},
}
-func TestClientSubscribeRecover(t *testing.T) {
+func TestClientSubscribeRecoverMemory(t *testing.T) {
for _, tt := range recoverTests {
t.Run(tt.Name, func(t *testing.T) {
node := nodeWithMemoryEngine()
@@ -456,25 +508,32 @@ func TestClientSubscribeRecover(t *testing.T) {
newCtx := SetCredentials(ctx, &Credentials{UserID: "42"})
client, _ := newClient(newCtx, node, transport)
- for i := 0; i < tt.NumPublications; i++ {
+ for i := 1; i <= tt.NumPublications; i++ {
node.Publish("test", &Publication{
- UID: strconv.Itoa(i),
Data: []byte(`{}`),
})
}
+ time.Sleep(time.Duration(tt.Sleep) * time.Second)
+
connectClient(t, client)
- subscribeResp, disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ recovery, _ := node.engine.historyRecoveryData("test")
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
Channel: "test",
Recover: true,
- Last: tt.Last,
- Since: tt.Since,
- })
+ Seq: tt.SinceSeq,
+ Gen: recovery.Gen,
+ Epoch: recovery.Epoch,
+ }, rw)
assert.Nil(t, disconnect)
- assert.Nil(t, subscribeResp.Error)
- assert.Equal(t, tt.NumRecovered, len(subscribeResp.Result.Publications))
- assert.Equal(t, tt.Recovered, subscribeResp.Result.Recovered)
+ assert.Nil(t, replies[0].Error)
+ res := extractSubscribeResult(replies)
+ assert.Equal(t, tt.NumRecovered, len(res.Publications))
+ assert.Equal(t, tt.Recovered, res.Recovered)
})
}
}
@@ -591,7 +650,7 @@ func TestClientPingWithRecover(t *testing.T) {
pingResp, disconnect := client.pingCmd(&proto.PingRequest{})
assert.Nil(t, disconnect)
assert.Nil(t, pingResp.Error)
- assert.NotZero(t, pingResp.Result.Time)
+ assert.Nil(t, pingResp.Result)
}
func TestClientPresence(t *testing.T) {
@@ -767,19 +826,39 @@ func TestClientHandleMalformedCommand(t *testing.T) {
connectClient(t, client)
- reply, disconnect := client.handle(&proto.Command{
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ disconnect := client.handle(&proto.Command{
ID: 1,
Method: 1000,
Params: []byte(`{}`),
- })
- assert.Equal(t, ErrorMethodNotFound, reply.Error)
+ }, rw.write, rw.flush)
assert.Nil(t, disconnect)
+ assert.Equal(t, ErrorMethodNotFound, replies[0].Error)
- reply, disconnect = client.handle(&proto.Command{
+ replies = nil
+ disconnect = client.handle(&proto.Command{
ID: 1,
Method: 2,
Params: []byte(`{}`),
- })
- assert.Nil(t, reply)
+ }, rw.write, rw.flush)
assert.Equal(t, DisconnectBadRequest, disconnect)
}
+
+func TestUnique(t *testing.T) {
+ pubs := []*Publication{
+ &Publication{Seq: 101, Gen: 0},
+ &Publication{Seq: 101, Gen: 1},
+ &Publication{Seq: 101, Gen: 1},
+ &Publication{Seq: 100, Gen: 2},
+ &Publication{Seq: 99},
+ &Publication{Seq: 98},
+ &Publication{Seq: 4294967295, Gen: 0},
+ &Publication{Seq: 4294967295, Gen: 1},
+ &Publication{Seq: 4294967295, Gen: 4294967295},
+ &Publication{Seq: 4294967295, Gen: 4294967295},
+ }
+ pubs = uniquePublications(pubs)
+ assert.Equal(t, 8, len(pubs))
+}
diff --git a/engine.go b/engine.go
index 18404732..05530c14 100644
--- a/engine.go
+++ b/engine.go
@@ -22,6 +22,13 @@ type EngineEventHandler interface {
HandleControl([]byte) error
}
+// recovery contains fields to rely in recovery process.
+type recovery struct {
+ Seq uint32
+ Gen uint32
+ Epoch string
+}
+
// Engine is responsible for PUB/SUB mechanics, channel history and
// presence information.
type Engine interface {
@@ -40,7 +47,6 @@ type Engine interface {
publishLeave(ch string, leave *Leave, opts *ChannelOptions) <-chan error
// PublishControl allows to send control command data to all running nodes.
publishControl(data []byte) <-chan error
-
// Subscribe node on channel to listen all messages coming from channel.
subscribe(ch string) error
// Unsubscribe node from channel to stop listening messages from it.
@@ -55,6 +61,8 @@ type Engine interface {
// messages (though limited by configured history_size). 1 means
// last (most recent) message only, 2 - two last messages etc.
history(ch string, limit int) ([]*Publication, error)
+ // historyRecoveryData allows to get current recovery state for channel.
+ historyRecoveryData(ch string) (recovery, error)
// recoverHistory allows to recover missed messages starting
// from last seen Publication UID provided by client. This method
// should return as many Publications as possible and boolean value
@@ -62,7 +70,7 @@ type Engine interface {
// The case when publications can not be fully restored
// can happen if old Publications already removed from history
// due to size or lifetime limits.
- recoverHistory(ch string, lastUID string) ([]*Publication, bool, error)
+ recoverHistory(ch string, since recovery) ([]*Publication, bool, recovery, error)
// RemoveHistory removes history from channel. This is in general not
// needed as history expires automatically (based on history_lifetime)
// but sometimes can be useful for application logic.
diff --git a/engine_memory.go b/engine_memory.go
index 0034c86e..f559eba6 100644
--- a/engine_memory.go
+++ b/engine_memory.go
@@ -2,6 +2,7 @@ package centrifuge
import (
"container/heap"
+ "strconv"
"sync"
"time"
@@ -40,6 +41,10 @@ func (e *MemoryEngine) run(h EngineEventHandler) error {
return nil
}
+// func (e *MemoryEngine) shutdown(h EngineEventHandler) error {
+// return e.db.Close()
+// }
+
// Publish adds message into history hub and calls node ClientMsg method to handle message.
// We don't have any PUB/SUB here as Memory Engine is single node only.
func (e *MemoryEngine) publish(ch string, pub *Publication, opts *ChannelOptions) <-chan error {
@@ -119,9 +124,15 @@ func (e *MemoryEngine) history(ch string, limit int) ([]*Publication, error) {
return e.historyHub.get(ch, limit)
}
+// History - see engine interface description.
+func (e *MemoryEngine) historyRecoveryData(ch string) (recovery, error) {
+ seq, gen, epoch := e.historyHub.getSequence(ch)
+ return recovery{seq, gen, epoch}, nil
+}
+
// RecoverHistory - see engine interface description.
-func (e *MemoryEngine) recoverHistory(ch string, lastUID string) ([]*Publication, bool, error) {
- return e.historyHub.recover(ch, lastUID)
+func (e *MemoryEngine) recoverHistory(ch string, since recovery) ([]*Publication, bool, recovery, error) {
+ return e.historyHub.recover(ch, since)
}
// RemoveHistory - see engine interface description.
@@ -232,11 +243,21 @@ func (i historyItem) isExpired() bool {
return i.expireAt < time.Now().Unix()
}
+type channelTop struct {
+ ID uint64
+ UID string
+}
+
type historyHub struct {
sync.RWMutex
history map[string]historyItem
queue priority.Queue
nextCheck int64
+
+ // db *badger.DB
+ epoch string
+ sequencesMu sync.RWMutex
+ sequences map[string]uint64
}
func newHistoryHub() *historyHub {
@@ -244,6 +265,9 @@ func newHistoryHub() *historyHub {
history: make(map[string]historyItem),
queue: priority.MakeQueue(),
nextCheck: 0,
+ // db: db,
+ epoch: strconv.FormatInt(time.Now().Unix(), 10),
+ sequences: make(map[string]uint64),
}
}
@@ -283,6 +307,39 @@ func (h *historyHub) expire() {
}
}
+func (h *historyHub) next(ch string) (uint32, uint32) {
+ var val uint64
+ h.sequencesMu.Lock()
+ top, ok := h.sequences[ch]
+ if !ok {
+ val = 1
+ h.sequences[ch] = val
+ } else {
+ top++
+ h.sequences[ch] = top
+ val = top
+ }
+ h.sequencesMu.Unlock()
+ return unpackUint64(val)
+}
+
+func unpackUint64(val uint64) (uint32, uint32) {
+ return uint32(val), uint32(val >> 32)
+}
+
+func (h *historyHub) getSequence(ch string) (uint32, uint32, string) {
+ h.sequencesMu.Lock()
+ defer h.sequencesMu.Unlock()
+ val, ok := h.sequences[ch]
+ if !ok {
+ var top uint64
+ h.sequences[ch] = top
+ return 0, 0, h.epoch
+ }
+ seq, gen := unpackUint64(val)
+ return seq, gen, h.epoch
+}
+
func (h *historyHub) touch(ch string, opts *ChannelOptions) {
h.Lock()
defer h.Unlock()
@@ -306,10 +363,12 @@ func (h *historyHub) touch(ch string, opts *ChannelOptions) {
}
}
-func (h *historyHub) add(ch string, msg *Publication, opts *ChannelOptions, hasSubscribers bool) error {
+func (h *historyHub) add(ch string, pub *Publication, opts *ChannelOptions, hasSubscribers bool) error {
h.Lock()
defer h.Unlock()
+ pub.Seq, pub.Gen = h.next(ch)
+
_, ok := h.history[ch]
if opts.HistoryDropInactive && !hasSubscribers && !ok {
@@ -321,12 +380,12 @@ func (h *historyHub) add(ch string, msg *Publication, opts *ChannelOptions, hasS
heap.Push(&h.queue, &priority.Item{Value: ch, Priority: expireAt})
if !ok {
h.history[ch] = historyItem{
- messages: []*Publication{msg},
+ messages: []*Publication{pub},
expireAt: expireAt,
}
} else {
messages := h.history[ch].messages
- messages = append([]*Publication{msg}, messages...)
+ messages = append([]*Publication{pub}, messages...)
if len(messages) > opts.HistorySize {
messages = messages[0:opts.HistorySize]
}
@@ -346,7 +405,10 @@ func (h *historyHub) add(ch string, msg *Publication, opts *ChannelOptions, hasS
func (h *historyHub) get(ch string, limit int) ([]*Publication, error) {
h.RLock()
defer h.RUnlock()
+ return h.getUnsafe(ch, limit)
+}
+func (h *historyHub) getUnsafe(ch string, limit int) ([]*Publication, error) {
hItem, ok := h.history[ch]
if !ok {
// return empty slice
@@ -358,8 +420,12 @@ func (h *historyHub) get(ch string, limit int) ([]*Publication, error) {
return []*Publication{}, nil
}
if limit == 0 || limit >= len(hItem.messages) {
- return hItem.messages, nil
+ pubs := make([]*Publication, len(hItem.messages))
+ copy(pubs, hItem.messages)
+ return pubs, nil
}
+ pubs := make([]*Publication, len(hItem.messages[:limit]))
+ copy(pubs, hItem.messages[:limit])
return hItem.messages[:limit], nil
}
@@ -374,29 +440,50 @@ func (h *historyHub) remove(ch string) error {
return nil
}
-func (h *historyHub) recover(ch string, last string) ([]*Publication, bool, error) {
+const (
+ maxSeq = 4294967295 // maximum uint32 value
+ maxGen = 4294967295 // maximum uint32 value
+)
- publications, err := h.get(ch, 0)
+func (h *historyHub) recover(ch string, since recovery) ([]*Publication, bool, recovery, error) {
+ h.RLock()
+ defer h.RUnlock()
+
+ currentSeq, currentGen, currentEpoch := h.getSequence(ch)
+
+ if currentSeq == since.Seq && since.Gen == currentGen && since.Epoch == currentEpoch {
+ return nil, true, recovery{currentSeq, currentGen, currentEpoch}, nil
+ }
+
+ publications, err := h.getUnsafe(ch, 0)
if err != nil {
- return nil, false, err
+ return nil, false, recovery{}, err
+ }
+
+ nextSeq := since.Seq + 1
+ nextGen := since.Gen
+
+ if nextSeq > maxSeq {
+ nextSeq = 0
+ nextGen = nextGen + 1
}
position := -1
- for index, msg := range publications {
- if msg.UID == last {
- position = index
+
+ for i := len(publications) - 1; i >= 0; i-- {
+ msg := publications[i]
+ if msg.Seq == since.Seq && msg.Gen == since.Gen {
+ position = i
+ break
+ }
+ if msg.Seq == nextSeq && msg.Gen == nextGen {
+ position = i + 1
break
}
}
if position > -1 {
- // Provided last UID found in history. In this case we can be
- // sure that all missed messages will be recovered.
- return publications[0:position], true, nil
+ return publications[0:position], since.Epoch == currentEpoch, recovery{currentSeq, currentGen, currentEpoch}, nil
}
- // Provided last UID not found in history messages. This means that
- // client most probably missed too many messages or publication with
- // last UID already expired (or maybe wrong last uid provided but
- // it's not a normal case). So we try to compensate as many as we
- // can but get caller know about missing UID.
- return publications, false, nil
+
+ return publications, false, recovery{currentSeq, currentGen, currentEpoch}, nil
}
diff --git a/engine_memory_test.go b/engine_memory_test.go
index 0f419d8c..896f9b90 100644
--- a/engine_memory_test.go
+++ b/engine_memory_test.go
@@ -2,7 +2,6 @@ package centrifuge
import (
"context"
- "strconv"
"testing"
"time"
@@ -361,14 +360,14 @@ func BenchmarkMemoryEngineHistoryRecoverParallel(b *testing.B) {
e := testMemoryEngine()
rawData := Raw([]byte("{}"))
numMessages := 100
- for i := 0; i < numMessages; i++ {
- pub := &Publication{UID: "uid" + strconv.Itoa(i), Data: rawData}
+ for i := 1; i <= numMessages; i++ {
+ pub := &Publication{Data: rawData}
<-e.publish("channel", pub, &ChannelOptions{HistorySize: numMessages, HistoryLifetime: 300, HistoryDropInactive: false})
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
- _, _, err := e.recoverHistory("channel", "uid"+strconv.Itoa(numMessages-5))
+ _, _, _, err := e.recoverHistory("channel", recovery{uint32(numMessages - 5), 0, ""})
if err != nil {
panic(err)
}
diff --git a/engine_redis.go b/engine_redis.go
index 4668b3ce..d9728950 100644
--- a/engine_redis.go
+++ b/engine_redis.go
@@ -1,6 +1,7 @@
package centrifuge
import (
+ "bytes"
"errors"
"fmt"
"hash/fnv"
@@ -84,6 +85,7 @@ type shard struct {
remPresenceScript *redis.Script
presenceScript *redis.Script
lpopManyScript *redis.Script
+ historySeqScript *redis.Script
messagePrefix string
pushEncoder proto.PushEncoder
@@ -331,24 +333,27 @@ var (
// 1 round trip to Redis instead of 2.
// KEYS[1] - history list key
// KEYS[2] - history touch object key
+ // KEYS[3] - history sequence key
// ARGV[1] - channel to publish message to
// ARGV[2] - message payload
// ARGV[3] - history size ltrim right bound
// ARGV[4] - history lifetime
// ARGV[5] - history drop inactive flag - "0" or "1"
pubScriptSource = `
-local n = redis.call("publish", ARGV[1], ARGV[2])
+local sequence = redis.call("incr", KEYS[3])
+local payload = sequence .. ":" .. ARGV[2]
+local num_subscribed_nodes = redis.call("publish", ARGV[1], payload)
local m = 0
-if ARGV[5] == "1" and n == 0 and redis.call("exists", KEYS[2]) == 0 then
- m = redis.call("lpushx", KEYS[1], ARGV[2])
+if ARGV[5] == "1" and num_subscribed_nodes == 0 and redis.call("exists", KEYS[2]) == 0 then
+ m = redis.call("lpushx", KEYS[1], payload)
else
- m = redis.call("lpush", KEYS[1], ARGV[2])
+ m = redis.call("lpush", KEYS[1], payload)
end
if m > 0 then
redis.call("ltrim", KEYS[1], 0, ARGV[3])
redis.call("expire", KEYS[1], ARGV[4])
end
-return n
+return num_subscribed_nodes
`
// KEYS[1] - presence set key
@@ -395,6 +400,21 @@ if #entries > 0 then
end
return entries
`
+
+ // KEYS[1] - history sequence key
+ // KEYS[2] - history gen key
+ historySeqSource = `
+redis.replicate_commands()
+local seq = redis.call("get", KEYS[1])
+local gen
+if redis.call('EXISTS', KEYS[2]) ~= 0 then
+ gen = redis.call("get", KEYS[2])
+else
+ gen = redis.call('TIME')[1]
+ redis.call("set", KEYS[2], gen)
+end
+return {seq, gen}
+ `
)
// newShard initializes new Redis shard.
@@ -403,11 +423,12 @@ func newShard(n *Node, conf RedisShardConfig) (*shard, error) {
node: n,
config: conf,
pool: newPool(n, conf),
- pubScript: redis.NewScript(2, pubScriptSource),
+ pubScript: redis.NewScript(3, pubScriptSource),
addPresenceScript: redis.NewScript(2, addPresenceSource),
remPresenceScript: redis.NewScript(2, remPresenceSource),
presenceScript: redis.NewScript(2, presenceSource),
lpopManyScript: redis.NewScript(1, lpopManySource),
+ historySeqScript: redis.NewScript(2, historySeqSource),
pushEncoder: proto.NewProtobufPushEncoder(),
pushDecoder: proto.NewProtobufPushDecoder(),
}
@@ -442,6 +463,14 @@ func (e *shard) getHistoryKey(ch string) channelID {
return channelID(e.config.Prefix + ".history.list." + ch)
}
+func (e *shard) gethistorySeqKey(ch string) channelID {
+ return channelID(e.config.Prefix + ".history.seq." + ch)
+}
+
+func (e *shard) gethistoryEpochKey(ch string) channelID {
+ return channelID(e.config.Prefix + ".history.epoch." + ch)
+}
+
func (e *shard) getHistoryTouchKey(ch string) channelID {
return channelID(e.config.Prefix + ".history.touch." + ch)
}
@@ -537,9 +566,14 @@ func (e *RedisEngine) history(ch string, limit int) ([]*Publication, error) {
return e.shards[e.shardIndex(ch)].History(ch, limit)
}
+// HistorySequence - see engine interface description.
+func (e *RedisEngine) historyRecoveryData(ch string) (recovery, error) {
+ return e.shards[e.shardIndex(ch)].HistorySequence(ch)
+}
+
// RecoverHistory - see engine interface description.
-func (e *RedisEngine) recoverHistory(ch string, lastUID string) ([]*Publication, bool, error) {
- return e.shards[e.shardIndex(ch)].RecoverHistory(ch, lastUID)
+func (e *RedisEngine) recoverHistory(ch string, since recovery) ([]*Publication, bool, recovery, error) {
+ return e.shards[e.shardIndex(ch)].RecoverHistory(ch, since)
}
// RemoveHistory - see engine interface description.
@@ -753,21 +787,23 @@ func (e *shard) runPubSub() {
}
func (e *shard) handleRedisClientMessage(chID channelID, data []byte) error {
- var message proto.Push
- err := message.Unmarshal(data)
+ parts := bytes.SplitN(data, []byte(":"), 2)
+ sequence, _ := strconv.ParseUint(string(parts[0]), 10, 64)
+ pushData := parts[1]
+ var push proto.Push
+ err := push.Unmarshal(pushData)
if err != nil {
return err
}
- return e.handleClientPush(&message)
-}
-
-func (e *shard) handleClientPush(push *proto.Push) error {
switch push.Type {
case proto.PushTypePublication:
pub, err := e.pushDecoder.DecodePublication(push.Data)
if err != nil {
return err
}
+ seq, gen := unpackUint64(sequence)
+ pub.Seq = seq
+ pub.Gen = gen
e.eventHandler.HandlePublication(push.Channel, pub)
case proto.PushTypeJoin:
join, err := e.pushDecoder.DecodeJoin(push.Data)
@@ -791,6 +827,7 @@ type pubRequest struct {
message []byte
historyKey channelID
touchKey channelID
+ indexKey channelID
opts *ChannelOptions
err *chan error
}
@@ -857,7 +894,7 @@ func (e *shard) runPublishPipeline() {
conn := e.pool.Get()
for i := range prs {
if prs[i].opts != nil && prs[i].opts.HistorySize > 0 && prs[i].opts.HistoryLifetime > 0 {
- e.pubScript.SendHash(conn, prs[i].historyKey, prs[i].touchKey, prs[i].channel, prs[i].message, prs[i].opts.HistorySize-1, prs[i].opts.HistoryLifetime, prs[i].opts.HistoryDropInactive)
+ e.pubScript.SendHash(conn, prs[i].historyKey, prs[i].touchKey, prs[i].indexKey, prs[i].channel, prs[i].message, prs[i].opts.HistorySize-1, prs[i].opts.HistoryLifetime, prs[i].opts.HistoryDropInactive)
} else {
conn.Send("PUBLISH", prs[i].channel, prs[i].message)
}
@@ -904,6 +941,7 @@ const (
dataOpRemovePresence
dataOpPresence
dataOpHistory
+ dataOphistorySeq
dataOpHistoryRemove
dataOpChannels
dataOpHistoryTouch
@@ -982,6 +1020,14 @@ func (e *shard) runDataPipeline() {
return
}
+ err = e.historySeqScript.Load(conn)
+ if err != nil {
+ e.node.logger.log(newLogEntry(LogLevelError, "error loading history seq Lua", map[string]interface{}{"error": err.Error()}))
+ // Can not proceed if script has not been loaded.
+ conn.Close()
+ return
+ }
+
conn.Close()
var drs []dataRequest
@@ -1002,6 +1048,8 @@ func (e *shard) runDataPipeline() {
e.presenceScript.SendHash(conn, drs[i].args...)
case dataOpHistory:
conn.Send("LRANGE", drs[i].args...)
+ case dataOphistorySeq:
+ e.historySeqScript.SendHash(conn, drs[i].args...)
case dataOpHistoryRemove:
conn.Send("DEL", drs[i].args...)
case dataOpChannels:
@@ -1069,6 +1117,7 @@ func (e *shard) Publish(ch string, pub *Publication, opts *ChannelOptions) <-cha
message: byteMessage,
historyKey: e.getHistoryKey(ch),
touchKey: e.getHistoryTouchKey(ch),
+ indexKey: e.gethistorySeqKey(ch),
opts: opts,
err: &eChan,
}
@@ -1105,7 +1154,7 @@ func (e *shard) PublishJoin(ch string, join *Join, opts *ChannelOptions) <-chan
pr := pubRequest{
channel: chID,
- message: byteMessage,
+ message: append([]byte("0:"), byteMessage...),
err: &eChan,
}
e.pubCh <- pr
@@ -1132,7 +1181,7 @@ func (e *shard) PublishLeave(ch string, leave *Leave, opts *ChannelOptions) <-ch
pr := pubRequest{
channel: chID,
- message: byteMessage,
+ message: append([]byte("0:"), byteMessage...),
err: &eChan,
}
e.pubCh <- pr
@@ -1262,30 +1311,83 @@ func (e *shard) History(ch string, limit int) ([]*Publication, error) {
return sliceOfPubs(e, resp.reply, nil)
}
+// History - see engine interface description.
+func (e *shard) HistorySequence(ch string) (recovery, error) {
+ historySeqKey := e.gethistorySeqKey(ch)
+ historyEpochKey := e.gethistoryEpochKey(ch)
+ dr := newDataRequest(dataOphistorySeq, []interface{}{historySeqKey, historyEpochKey}, true)
+ e.dataCh <- dr
+ resp := dr.result()
+ if resp.err != nil {
+ return recovery{}, resp.err
+ }
+ results := resp.reply.([]interface{})
+
+ sequence, err := redis.Int64(results[0], nil)
+ if err != nil {
+ if err != redis.ErrNil {
+ return recovery{}, err
+ }
+ sequence = 0
+ }
+
+ seq, gen := unpackUint64(uint64(sequence))
+
+ var epoch string
+ epoch, err = redis.String(results[1], nil)
+ if err != nil {
+ if err != redis.ErrNil {
+ return recovery{}, err
+ }
+ epoch = ""
+ }
+
+ return recovery{seq, gen, epoch}, nil
+}
+
// RecoverHistory - see engine interface description.
-func (e *shard) RecoverHistory(ch string, last string) ([]*Publication, bool, error) {
+func (e *shard) RecoverHistory(ch string, since recovery) ([]*Publication, bool, recovery, error) {
+
+ currentRecovery, err := e.HistorySequence(ch)
+ if err != nil {
+ return nil, false, recovery{}, err
+ }
+
+ if currentRecovery.Seq == since.Seq && since.Gen == currentRecovery.Gen && since.Epoch == currentRecovery.Epoch {
+ return nil, true, currentRecovery, nil
+ }
+
publications, err := e.History(ch, 0)
if err != nil {
- return nil, false, err
+ return nil, false, recovery{}, err
+ }
+
+ nextSeq := since.Seq + 1
+ nextGen := since.Gen
+
+ if nextSeq > maxSeq {
+ nextSeq = 0
+ nextGen = nextGen + 1
}
position := -1
- for index, msg := range publications {
- if msg.UID == last {
- position = index
+
+ for i := len(publications) - 1; i >= 0; i-- {
+ msg := publications[i]
+ if msg.Seq == since.Seq && msg.Gen == since.Gen {
+ position = i
+ break
+ }
+ if msg.Seq == nextSeq && msg.Gen == nextGen {
+ position = i + 1
break
}
}
if position > -1 {
- // Last uid found in history.
- return publications[0:position], true, nil
+ return publications[0:position], currentRecovery.Epoch == since.Epoch, currentRecovery, nil
}
- // Provided last UID not found in history messages. This means that
- // client most probably missed too many messages or publication with
- // last UID already expired (or maybe wrong last uid provided but
- // it's not a normal case). So we try to compensate as many as we
- // can but get caller know about missing UID.
- return publications, false, nil
+
+ return publications, false, currentRecovery, nil
}
// RemoveHistory - see engine interface description.
@@ -1362,7 +1464,11 @@ func sliceOfPubs(n *shard, result interface{}, err error) ([]*Publication, error
return nil, errors.New("error getting Message value")
}
- msg, err := n.pushDecoder.Decode(value)
+ parts := bytes.SplitN(value, []byte(":"), 2)
+ sequence, _ := strconv.ParseUint(string(parts[0]), 10, 64)
+ pushData := parts[1]
+
+ msg, err := n.pushDecoder.Decode(pushData)
if err != nil {
return nil, fmt.Errorf("can not unmarshal value to Message: %v", err)
}
@@ -1375,6 +1481,10 @@ func sliceOfPubs(n *shard, result interface{}, err error) ([]*Publication, error
if err != nil {
return nil, fmt.Errorf("can not unmarshal value to Pub: %v", err)
}
+
+ seq, gen := unpackUint64(sequence)
+ publication.Seq = seq
+ publication.Gen = gen
msgs[i] = publication
}
return msgs, nil
diff --git a/engine_redis_test.go b/engine_redis_test.go
index f51e22d8..b5f64957 100644
--- a/engine_redis_test.go
+++ b/engine_redis_test.go
@@ -3,6 +3,7 @@
package centrifuge
import (
+ "context"
"errors"
"fmt"
"math/rand"
@@ -12,6 +13,7 @@ import (
"testing"
"time"
+ "github.com/centrifugal/centrifuge/internal/proto"
"github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
)
@@ -248,21 +250,24 @@ func TestRedisEngineRecover(t *testing.T) {
pub.UID = "5"
assert.NoError(t, nil, <-e.publish("channel", pub, &ChannelOptions{HistorySize: 10, HistoryLifetime: 2}))
- pubs, recovered, err := e.recoverHistory("channel", "2")
+ r, err := e.historyRecoveryData("channel")
+ assert.NoError(t, err)
+
+ pubs, recovered, _, err := e.recoverHistory("channel", recovery{2, 0, r.Epoch})
assert.NoError(t, err)
assert.True(t, recovered)
assert.Equal(t, 3, len(pubs))
- assert.Equal(t, "5", pubs[0].UID)
- assert.Equal(t, "4", pubs[1].UID)
- assert.Equal(t, "3", pubs[2].UID)
+ assert.Equal(t, uint32(5), pubs[0].Seq)
+ assert.Equal(t, uint32(4), pubs[1].Seq)
+ assert.Equal(t, uint32(3), pubs[2].Seq)
- pubs, recovered, err = e.recoverHistory("channel", "6")
+ pubs, recovered, _, err = e.recoverHistory("channel", recovery{6, 0, r.Epoch})
assert.NoError(t, err)
assert.False(t, recovered)
assert.Equal(t, 5, len(pubs))
assert.NoError(t, e.removeHistory("channel"))
- pubs, recovered, err = e.recoverHistory("channel", "2")
+ pubs, recovered, _, err = e.recoverHistory("channel", recovery{2, 0, r.Epoch})
assert.NoError(t, err)
assert.False(t, recovered)
assert.Equal(t, 0, len(pubs))
@@ -632,16 +637,68 @@ func BenchmarkRedisEngineHistoryRecoverParallel(b *testing.B) {
rawData := Raw([]byte("{}"))
numMessages := 100
for i := 0; i < numMessages; i++ {
- pub := &Publication{UID: "uid" + strconv.Itoa(i), Data: rawData}
+ pub := &Publication{Data: rawData}
<-e.publish("channel", pub, &ChannelOptions{HistorySize: numMessages, HistoryLifetime: 300, HistoryDropInactive: false})
}
+ r, err := e.historyRecoveryData("channel")
+ assert.NoError(b, err)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
- _, _, err := e.recoverHistory("channel", "uid"+strconv.Itoa(numMessages-5))
+ _, _, _, err := e.recoverHistory("channel", recovery{uint32(numMessages - 5), 0, r.Epoch})
if err != nil {
panic(err)
}
}
})
}
+
+func TestClientSubscribeRecoverRedis(t *testing.T) {
+ for _, tt := range recoverTests {
+ t.Run(tt.Name, func(t *testing.T) {
+ c := dial()
+ defer c.close()
+
+ e := newTestRedisEngine()
+
+ config := e.node.Config()
+ config.HistorySize = tt.HistorySize
+ config.HistoryLifetime = tt.HistoryLifetime
+ config.HistoryRecover = true
+ e.node.Reload(config)
+
+ transport := newTestTransport()
+ ctx := context.Background()
+ newCtx := SetCredentials(ctx, &Credentials{UserID: "42"})
+ client, _ := newClient(newCtx, e.node, transport)
+
+ for i := 1; i <= tt.NumPublications; i++ {
+ e.node.Publish("test", &Publication{
+ UID: strconv.Itoa(i),
+ Data: []byte(`{}`),
+ })
+ }
+
+ time.Sleep(time.Duration(tt.Sleep) * time.Second)
+
+ connectClient(t, client)
+
+ replies := []*proto.Reply{}
+ rw := testReplyWriter(&replies)
+
+ recovery, _ := e.historyRecoveryData("test")
+ disconnect := client.subscribeCmd(&proto.SubscribeRequest{
+ Channel: "test",
+ Recover: true,
+ Seq: tt.SinceSeq,
+ Gen: recovery.Gen,
+ Epoch: recovery.Epoch,
+ }, rw)
+ assert.Nil(t, disconnect)
+ assert.Nil(t, replies[0].Error)
+ res := extractSubscribeResult(replies)
+ assert.Equal(t, tt.NumRecovered, len(res.Publications))
+ assert.Equal(t, tt.Recovered, res.Recovered)
+ })
+ }
+}
diff --git a/engine_test.go b/engine_test.go
new file mode 100644
index 00000000..7610b1f4
--- /dev/null
+++ b/engine_test.go
@@ -0,0 +1,230 @@
+package centrifuge
+
+import (
+ "testing"
+ "time"
+
+ "github.com/centrifugal/centrifuge/internal/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+type testMessage struct {
+ Sequence int
+ Generation string
+}
+
+type testStore struct {
+ Sequence int
+ Generation string
+ messages []testMessage
+ lifetime time.Duration
+ size int
+ expireAt time.Time
+}
+
+func newTestStore() *testStore {
+ return &testStore{
+ Sequence: 0,
+ Generation: uuid.Must(uuid.NewV4()).String(),
+ messages: make([]testMessage, 0),
+ lifetime: 500 * time.Millisecond,
+ size: 5,
+ }
+}
+
+func (s *testStore) add(m testMessage) (int, string) {
+ s.Sequence++
+ m.Sequence = s.Sequence
+ m.Generation = s.Generation
+ s.messages = append(s.messages, m)
+ if len(s.messages) > s.size {
+ s.messages = s.messages[1:]
+ }
+ s.expireAt = time.Now().Add(s.lifetime)
+ return s.Sequence, s.Generation
+}
+
+func (s *testStore) get() []testMessage {
+ if time.Now().Sub(s.expireAt) < 0 {
+ return s.messages
+ }
+ return nil
+}
+
+func (s *testStore) last() (int, string) {
+ return s.Sequence, s.Generation
+}
+
+func (s *testStore) recover(seq int, gen string) ([]testMessage, bool) {
+ // Following 2 lines must be atomic.
+ lastSeq, currentGen := s.last()
+ messages := s.get()
+
+ broken := false
+ position := -1
+
+ if lastSeq == seq && gen == currentGen {
+ return nil, true
+ }
+
+ for i := 0; i < len(messages); i++ {
+ msg := messages[i]
+ if msg.Sequence == seq {
+ if msg.Generation != gen {
+ broken = true
+ }
+ position = i + 1
+ break
+ }
+ if msg.Sequence == seq+1 {
+ if msg.Generation != gen {
+ broken = true
+ }
+ position = i
+ break
+ }
+ }
+ if position > -1 {
+ return messages[position:], !broken
+ }
+
+ return messages, false
+}
+
+func TestCaseAbstractRecover01(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.last()
+ assert.Equal(t, seq, 0)
+ assert.NotEmpty(t, gen)
+
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.True(t, recovered)
+
+ s.add(testMessage{Generation: "1"})
+ messages, recovered = s.recover(seq, gen)
+ assert.Equal(t, 1, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover02(t *testing.T) {
+ s := newTestStore()
+ s.add(testMessage{})
+ messages, recovered := s.recover(0, "1212")
+ assert.Equal(t, 1, len(messages))
+ assert.False(t, recovered)
+}
+
+func TestCaseAbstractRecover03(t *testing.T) {
+ s := newTestStore()
+ seq1, gen := s.add(testMessage{})
+ _, gen = s.add(testMessage{})
+ messages, recovered := s.recover(seq1, gen)
+ assert.Equal(t, 1, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover04(t *testing.T) {
+ s := newTestStore()
+ seq1, gen := s.add(testMessage{})
+ for i := 0; i < 6; i++ {
+ _, gen = s.add(testMessage{})
+ }
+ messages, recovered := s.recover(seq1, gen)
+ assert.Equal(t, 5, len(messages))
+ assert.False(t, recovered)
+}
+
+func TestCaseAbstractRecover05(t *testing.T) {
+ s := newTestStore()
+ seq1, gen := s.add(testMessage{})
+ for i := 0; i < 4; i++ {
+ _, gen = s.add(testMessage{})
+ }
+ messages, recovered := s.recover(seq1, gen)
+ assert.Equal(t, 4, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover06(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.add(testMessage{})
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover07(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.add(testMessage{})
+ time.Sleep(time.Second)
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover08(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.add(testMessage{})
+
+ s = newTestStore()
+ s.add(testMessage{})
+
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.False(t, recovered)
+}
+
+func TestCaseAbstractRecover09(t *testing.T) {
+ // What if we use new Store but with data saved from old one.
+ s := newTestStore()
+ seq, gen := s.add(testMessage{})
+ oldMessages := s.messages
+ oldExpireAt := s.expireAt
+
+ s = newTestStore()
+ s.Sequence = seq
+ s.Generation = gen
+ s.messages = oldMessages
+ s.expireAt = oldExpireAt
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.True(t, recovered)
+}
+
+func TestCaseAbstractRecover10(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.last()
+ s.add(testMessage{})
+ time.Sleep(time.Second)
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 0, len(messages))
+ assert.False(t, recovered)
+}
+
+func TestCaseAbstractRecover11(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.last()
+ for i := 0; i < 7; i++ {
+ s.add(testMessage{})
+ }
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 5, len(messages))
+ assert.False(t, recovered)
+}
+
+func TestCaseAbstractRecover12(t *testing.T) {
+ s := newTestStore()
+ seq, gen := s.last()
+ for i := 0; i < 3; i++ {
+ s.add(testMessage{})
+ }
+ time.Sleep(time.Second)
+ s.messages = nil
+ for i := 0; i < 3; i++ {
+ s.add(testMessage{})
+ }
+ messages, recovered := s.recover(seq, gen)
+ assert.Equal(t, 3, len(messages))
+ assert.False(t, recovered)
+}
diff --git a/examples/chat_json/index.html b/examples/chat_json/index.html
index d90155dc..c8bc45c4 100644
--- a/examples/chat_json/index.html
+++ b/examples/chat_json/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/examples/chat_json/main.go b/examples/chat_json/main.go
index 1287583d..ee214d53 100644
--- a/examples/chat_json/main.go
+++ b/examples/chat_json/main.go
@@ -54,6 +54,7 @@ func main() {
// not required if you don't use token authentication and
// private subscriptions verified by token.
cfg.Secret = "secret"
+ cfg.Publish = true
cfg.Namespaces = []centrifuge.ChannelNamespace{
centrifuge.ChannelNamespace{
@@ -63,7 +64,7 @@ func main() {
Presence: true,
JoinLeave: true,
HistoryLifetime: 60,
- HistorySize: 10,
+ HistorySize: 1000,
HistoryRecover: true,
},
},
@@ -142,6 +143,19 @@ func main() {
node.SetLogHandler(centrifuge.LogLevelDebug, handleLog)
+ engine, err := centrifuge.NewRedisEngine(node, centrifuge.RedisEngineConfig{
+ Shards: []centrifuge.RedisShardConfig{
+ centrifuge.RedisShardConfig{
+ Host: "localhost",
+ Port: 6379,
+ },
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+ node.SetEngine(engine)
+
if err := node.Run(); err != nil {
panic(err)
}
diff --git a/handler_sockjs.go b/handler_sockjs.go
index 2b130ff4..6ee139ef 100644
--- a/handler_sockjs.go
+++ b/handler_sockjs.go
@@ -206,7 +206,7 @@ func (s *SockjsHandler) sockJSHandler(sess sockjs.Session) {
for {
if msg, err := sess.Recv(); err == nil {
- ok := handleClientData(s.node, c, []byte(msg), transport, writer)
+ ok := c.handleRawData([]byte(msg), writer)
if !ok {
return
}
diff --git a/handler_websocket.go b/handler_websocket.go
index 9f18ddd8..5f0611e8 100644
--- a/handler_websocket.go
+++ b/handler_websocket.go
@@ -2,7 +2,6 @@ package centrifuge
import (
"encoding/json"
- "fmt"
"io"
"net/http"
"sync"
@@ -318,77 +317,10 @@ func (s *WebsocketHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
- ok := handleClientData(s.node, c, data, transport, writer)
+ ok := c.handleRawData(data, writer)
if !ok {
return
}
}
}()
}
-
-// common data handling logic for Websocket and Sockjs handlers.
-func handleClientData(n *Node, c *Client, data []byte, transport Transport, writer *writer) bool {
- if len(data) == 0 {
- n.logger.log(newLogEntry(LogLevelError, "empty client request received"))
- c.close(DisconnectBadRequest)
- return false
- }
-
- enc := transport.Encoding()
-
- encoder := proto.GetReplyEncoder(enc)
- decoder := proto.GetCommandDecoder(enc, data)
- var numReplies int
-
- for {
- cmd, err := decoder.Decode()
- if err != nil {
- if err == io.EOF {
- break
- }
- n.logger.log(newLogEntry(LogLevelInfo, "error decoding command", map[string]interface{}{"data": string(data), "client": c.ID(), "user": c.UserID(), "error": err.Error()}))
- c.close(DisconnectBadRequest)
- proto.PutCommandDecoder(enc, decoder)
- proto.PutReplyEncoder(enc, encoder)
- return false
- }
- rep, disconnect := c.handle(cmd)
- if disconnect != nil {
- n.logger.log(newLogEntry(LogLevelInfo, "disconnect after handling command", map[string]interface{}{"command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "reason": disconnect.Reason}))
- c.close(disconnect)
- proto.PutCommandDecoder(enc, decoder)
- proto.PutReplyEncoder(enc, encoder)
- return false
- }
- if rep != nil {
- if rep.Error != nil {
- n.logger.log(newLogEntry(LogLevelInfo, "error in reply", map[string]interface{}{"reply": fmt.Sprintf("%v", rep), "command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "error": rep.Error.Error()}))
- }
- err = encoder.Encode(rep)
- numReplies++
- if err != nil {
- n.logger.log(newLogEntry(LogLevelError, "error encoding reply", map[string]interface{}{"reply": fmt.Sprintf("%v", rep), "command": fmt.Sprintf("%v", cmd), "client": c.ID(), "user": c.UserID(), "error": err.Error()}))
- c.close(DisconnectServerError)
- return false
- }
- }
- }
-
- if numReplies > 0 {
- disconnect := writer.write(encoder.Finish())
- if disconnect != nil {
- if n.logger.enabled(LogLevelDebug) {
- n.logger.log(newLogEntry(LogLevelDebug, "disconnect after sending reply", map[string]interface{}{"client": c.ID(), "user": c.UserID(), "reason": disconnect.Reason}))
- }
- c.close(disconnect)
- proto.PutCommandDecoder(enc, decoder)
- proto.PutReplyEncoder(enc, encoder)
- return false
- }
- }
-
- proto.PutCommandDecoder(enc, decoder)
- proto.PutReplyEncoder(enc, encoder)
-
- return true
-}
diff --git a/hub.go b/hub.go
index ad64c8a0..d60bb313 100644
--- a/hub.go
+++ b/hub.go
@@ -259,7 +259,7 @@ func (h *Hub) broadcastPublication(channel string, pub *Publication) error {
}
jsonReply = newPreparedReply(reply, proto.EncodingJSON)
}
- c.transport.Send(jsonReply)
+ c.writePublication(channel, pub, jsonReply)
} else if enc == proto.EncodingProtobuf {
if protobufReply == nil {
data, err := proto.GetPushEncoder(enc).EncodePublication(pub)
@@ -275,7 +275,7 @@ func (h *Hub) broadcastPublication(channel string, pub *Publication) error {
}
protobufReply = newPreparedReply(reply, proto.EncodingProtobuf)
}
- c.transport.Send(protobufReply)
+ c.writePublication(channel, pub, protobufReply)
}
}
return nil
@@ -317,7 +317,7 @@ func (h *Hub) broadcastJoin(channel string, join *proto.Join) error {
}
jsonReply = newPreparedReply(reply, proto.EncodingJSON)
}
- c.transport.Send(jsonReply)
+ c.writeJoin(channel, jsonReply)
} else if enc == proto.EncodingProtobuf {
if protobufReply == nil {
data, err := proto.GetPushEncoder(enc).EncodeJoin(join)
@@ -333,7 +333,7 @@ func (h *Hub) broadcastJoin(channel string, join *proto.Join) error {
}
protobufReply = newPreparedReply(reply, proto.EncodingProtobuf)
}
- c.transport.Send(protobufReply)
+ c.writeJoin(channel, protobufReply)
}
}
return nil
@@ -375,7 +375,7 @@ func (h *Hub) broadcastLeave(channel string, leave *proto.Leave) error {
}
jsonReply = newPreparedReply(reply, proto.EncodingJSON)
}
- c.transport.Send(jsonReply)
+ c.writeLeave(channel, jsonReply)
} else if enc == proto.EncodingProtobuf {
if protobufReply == nil {
data, err := proto.GetPushEncoder(enc).EncodeLeave(leave)
@@ -391,7 +391,7 @@ func (h *Hub) broadcastLeave(channel string, leave *proto.Leave) error {
}
protobufReply = newPreparedReply(reply, proto.EncodingProtobuf)
}
- c.transport.Send(protobufReply)
+ c.writeLeave(channel, protobufReply)
}
}
return nil
diff --git a/internal/proto/client.pb.go b/internal/proto/client.pb.go
index d97df81f..0abdb5e7 100644
--- a/internal/proto/client.pb.go
+++ b/internal/proto/client.pb.go
@@ -269,9 +269,11 @@ func (m *ClientInfo) GetClient() string {
}
type Publication struct {
- UID string `protobuf:"bytes,1,opt,name=uid,proto3" json:"uid,omitempty"`
- Data Raw `protobuf:"bytes,2,opt,name=data,proto3,customtype=Raw" json:"data"`
- Info *ClientInfo `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"`
+ Seq uint32 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
+ Gen uint32 `protobuf:"varint,2,opt,name=gen,proto3" json:"gen,omitempty"`
+ UID string `protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"`
+ Data Raw `protobuf:"bytes,4,opt,name=data,proto3,customtype=Raw" json:"data"`
+ Info *ClientInfo `protobuf:"bytes,5,opt,name=info" json:"info,omitempty"`
}
func (m *Publication) Reset() { *m = Publication{} }
@@ -279,6 +281,20 @@ func (m *Publication) String() string { return proto1.CompactTextStri
func (*Publication) ProtoMessage() {}
func (*Publication) Descriptor() ([]byte, []int) { return fileDescriptorClient, []int{5} }
+func (m *Publication) GetSeq() uint32 {
+ if m != nil {
+ return m.Seq
+ }
+ return 0
+}
+
+func (m *Publication) GetGen() uint32 {
+ if m != nil {
+ return m.Gen
+ }
+ return 0
+}
+
func (m *Publication) GetUID() string {
if m != nil {
return m.UID
@@ -373,7 +389,6 @@ type ConnectResult struct {
Expires bool `protobuf:"varint,3,opt,name=expires,proto3" json:"expires,omitempty"`
TTL uint32 `protobuf:"varint,4,opt,name=ttl,proto3" json:"ttl,omitempty"`
Data Raw `protobuf:"bytes,5,opt,name=data,proto3,customtype=Raw" json:"data,omitempty"`
- Time uint32 `protobuf:"varint,6,opt,name=time,proto3" json:"time,omitempty"`
}
func (m *ConnectResult) Reset() { *m = ConnectResult{} }
@@ -409,13 +424,6 @@ func (m *ConnectResult) GetTTL() uint32 {
return 0
}
-func (m *ConnectResult) GetTime() uint32 {
- if m != nil {
- return m.Time
- }
- return 0
-}
-
type RefreshRequest struct {
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token"`
}
@@ -474,10 +482,11 @@ func (m *RefreshResult) GetTTL() uint32 {
type SubscribeRequest struct {
Channel string `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel"`
- Recover bool `protobuf:"varint,2,opt,name=recover,proto3" json:"recover"`
- Last string `protobuf:"bytes,3,opt,name=last,proto3" json:"last"`
- Since uint32 `protobuf:"varint,4,opt,name=since,proto3" json:"since"`
- Token string `protobuf:"bytes,5,opt,name=token,proto3" json:"token"`
+ Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token"`
+ Recover bool `protobuf:"varint,3,opt,name=recover,proto3" json:"recover"`
+ Seq uint32 `protobuf:"varint,4,opt,name=seq,proto3" json:"seq"`
+ Gen uint32 `protobuf:"varint,5,opt,name=gen,proto3" json:"gen"`
+ Epoch string `protobuf:"bytes,6,opt,name=epoch,proto3" json:"epoch"`
}
func (m *SubscribeRequest) Reset() { *m = SubscribeRequest{} }
@@ -492,6 +501,13 @@ func (m *SubscribeRequest) GetChannel() string {
return ""
}
+func (m *SubscribeRequest) GetToken() string {
+ if m != nil {
+ return m.Token
+ }
+ return ""
+}
+
func (m *SubscribeRequest) GetRecover() bool {
if m != nil {
return m.Recover
@@ -499,34 +515,36 @@ func (m *SubscribeRequest) GetRecover() bool {
return false
}
-func (m *SubscribeRequest) GetLast() string {
+func (m *SubscribeRequest) GetSeq() uint32 {
if m != nil {
- return m.Last
+ return m.Seq
}
- return ""
+ return 0
}
-func (m *SubscribeRequest) GetSince() uint32 {
+func (m *SubscribeRequest) GetGen() uint32 {
if m != nil {
- return m.Since
+ return m.Gen
}
return 0
}
-func (m *SubscribeRequest) GetToken() string {
+func (m *SubscribeRequest) GetEpoch() string {
if m != nil {
- return m.Token
+ return m.Epoch
}
return ""
}
type SubscribeResult struct {
- Last string `protobuf:"bytes,1,opt,name=last,proto3" json:"last,omitempty"`
- Recovered bool `protobuf:"varint,2,opt,name=recovered,proto3" json:"recovered,omitempty"`
- Publications []*Publication `protobuf:"bytes,3,rep,name=publications" json:"publications,omitempty"`
- Expires bool `protobuf:"varint,4,opt,name=expires,proto3" json:"expires,omitempty"`
- TTL uint32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"`
- Time uint32 `protobuf:"varint,6,opt,name=time,proto3" json:"time,omitempty"`
+ Expires bool `protobuf:"varint,1,opt,name=expires,proto3" json:"expires,omitempty"`
+ TTL uint32 `protobuf:"varint,2,opt,name=ttl,proto3" json:"ttl,omitempty"`
+ Recoverable bool `protobuf:"varint,3,opt,name=recoverable,proto3" json:"recoverable,omitempty"`
+ Seq uint32 `protobuf:"varint,4,opt,name=seq,proto3" json:"seq,omitempty"`
+ Gen uint32 `protobuf:"varint,5,opt,name=gen,proto3" json:"gen,omitempty"`
+ Epoch string `protobuf:"bytes,6,opt,name=epoch,proto3" json:"epoch,omitempty"`
+ Publications []*Publication `protobuf:"bytes,7,rep,name=publications" json:"publications,omitempty"`
+ Recovered bool `protobuf:"varint,8,opt,name=recovered,proto3" json:"recovered,omitempty"`
}
func (m *SubscribeResult) Reset() { *m = SubscribeResult{} }
@@ -534,46 +552,60 @@ func (m *SubscribeResult) String() string { return proto1.CompactText
func (*SubscribeResult) ProtoMessage() {}
func (*SubscribeResult) Descriptor() ([]byte, []int) { return fileDescriptorClient, []int{15} }
-func (m *SubscribeResult) GetLast() string {
+func (m *SubscribeResult) GetExpires() bool {
if m != nil {
- return m.Last
+ return m.Expires
}
- return ""
+ return false
}
-func (m *SubscribeResult) GetRecovered() bool {
+func (m *SubscribeResult) GetTTL() uint32 {
if m != nil {
- return m.Recovered
+ return m.TTL
}
- return false
+ return 0
}
-func (m *SubscribeResult) GetPublications() []*Publication {
+func (m *SubscribeResult) GetRecoverable() bool {
if m != nil {
- return m.Publications
+ return m.Recoverable
}
- return nil
+ return false
}
-func (m *SubscribeResult) GetExpires() bool {
+func (m *SubscribeResult) GetSeq() uint32 {
if m != nil {
- return m.Expires
+ return m.Seq
}
- return false
+ return 0
}
-func (m *SubscribeResult) GetTTL() uint32 {
+func (m *SubscribeResult) GetGen() uint32 {
if m != nil {
- return m.TTL
+ return m.Gen
}
return 0
}
-func (m *SubscribeResult) GetTime() uint32 {
+func (m *SubscribeResult) GetEpoch() string {
if m != nil {
- return m.Time
+ return m.Epoch
}
- return 0
+ return ""
+}
+
+func (m *SubscribeResult) GetPublications() []*Publication {
+ if m != nil {
+ return m.Publications
+ }
+ return nil
+}
+
+func (m *SubscribeResult) GetRecovered() bool {
+ if m != nil {
+ return m.Recovered
+ }
+ return false
}
type SubRefreshRequest struct {
@@ -786,7 +818,6 @@ func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) { return fileDescriptorClient, []int{28} }
type PingResult struct {
- Time uint32 `protobuf:"varint,1,opt,name=time,proto3" json:"time,omitempty"`
}
func (m *PingResult) Reset() { *m = PingResult{} }
@@ -794,13 +825,6 @@ func (m *PingResult) String() string { return proto1.CompactTextStrin
func (*PingResult) ProtoMessage() {}
func (*PingResult) Descriptor() ([]byte, []int) { return fileDescriptorClient, []int{29} }
-func (m *PingResult) GetTime() uint32 {
- if m != nil {
- return m.Time
- }
- return 0
-}
-
type RPCRequest struct {
Data Raw `protobuf:"bytes,1,opt,name=data,proto3,customtype=Raw" json:"data"`
}
@@ -1034,6 +1058,12 @@ func (this *Publication) Equal(that interface{}) bool {
} else if this == nil {
return false
}
+ if this.Seq != that1.Seq {
+ return false
+ }
+ if this.Gen != that1.Gen {
+ return false
+ }
if this.UID != that1.UID {
return false
}
@@ -1202,9 +1232,6 @@ func (this *ConnectResult) Equal(that interface{}) bool {
if !this.Data.Equal(that1.Data) {
return false
}
- if this.Time != that1.Time {
- return false
- }
return true
}
func (this *RefreshRequest) Equal(that interface{}) bool {
@@ -1286,16 +1313,19 @@ func (this *SubscribeRequest) Equal(that interface{}) bool {
if this.Channel != that1.Channel {
return false
}
+ if this.Token != that1.Token {
+ return false
+ }
if this.Recover != that1.Recover {
return false
}
- if this.Last != that1.Last {
+ if this.Seq != that1.Seq {
return false
}
- if this.Since != that1.Since {
+ if this.Gen != that1.Gen {
return false
}
- if this.Token != that1.Token {
+ if this.Epoch != that1.Epoch {
return false
}
return true
@@ -1319,10 +1349,22 @@ func (this *SubscribeResult) Equal(that interface{}) bool {
} else if this == nil {
return false
}
- if this.Last != that1.Last {
+ if this.Expires != that1.Expires {
return false
}
- if this.Recovered != that1.Recovered {
+ if this.TTL != that1.TTL {
+ return false
+ }
+ if this.Recoverable != that1.Recoverable {
+ return false
+ }
+ if this.Seq != that1.Seq {
+ return false
+ }
+ if this.Gen != that1.Gen {
+ return false
+ }
+ if this.Epoch != that1.Epoch {
return false
}
if len(this.Publications) != len(that1.Publications) {
@@ -1333,13 +1375,7 @@ func (this *SubscribeResult) Equal(that interface{}) bool {
return false
}
}
- if this.Expires != that1.Expires {
- return false
- }
- if this.TTL != that1.TTL {
- return false
- }
- if this.Time != that1.Time {
+ if this.Recovered != that1.Recovered {
return false
}
return true
@@ -1688,9 +1724,6 @@ func (this *PingResult) Equal(that interface{}) bool {
} else if this == nil {
return false
}
- if this.Time != that1.Time {
- return false
- }
return true
}
func (this *RPCRequest) Equal(that interface{}) bool {
@@ -1969,13 +2002,23 @@ func (m *Publication) MarshalTo(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
+ if m.Seq != 0 {
+ dAtA[i] = 0x8
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(m.Seq))
+ }
+ if m.Gen != 0 {
+ dAtA[i] = 0x10
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(m.Gen))
+ }
if len(m.UID) > 0 {
- dAtA[i] = 0xa
+ dAtA[i] = 0x1a
i++
i = encodeVarintClient(dAtA, i, uint64(len(m.UID)))
i += copy(dAtA[i:], m.UID)
}
- dAtA[i] = 0x12
+ dAtA[i] = 0x22
i++
i = encodeVarintClient(dAtA, i, uint64(m.Data.Size()))
n7, err := m.Data.MarshalTo(dAtA[i:])
@@ -1984,7 +2027,7 @@ func (m *Publication) MarshalTo(dAtA []byte) (int, error) {
}
i += n7
if m.Info != nil {
- dAtA[i] = 0x1a
+ dAtA[i] = 0x2a
i++
i = encodeVarintClient(dAtA, i, uint64(m.Info.Size()))
n8, err := m.Info.MarshalTo(dAtA[i:])
@@ -2184,11 +2227,6 @@ func (m *ConnectResult) MarshalTo(dAtA []byte) (int, error) {
return 0, err
}
i += n13
- if m.Time != 0 {
- dAtA[i] = 0x30
- i++
- i = encodeVarintClient(dAtA, i, uint64(m.Time))
- }
return i, nil
}
@@ -2282,8 +2320,14 @@ func (m *SubscribeRequest) MarshalTo(dAtA []byte) (int, error) {
i = encodeVarintClient(dAtA, i, uint64(len(m.Channel)))
i += copy(dAtA[i:], m.Channel)
}
+ if len(m.Token) > 0 {
+ dAtA[i] = 0x12
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(len(m.Token)))
+ i += copy(dAtA[i:], m.Token)
+ }
if m.Recover {
- dAtA[i] = 0x10
+ dAtA[i] = 0x18
i++
if m.Recover {
dAtA[i] = 1
@@ -2292,22 +2336,21 @@ func (m *SubscribeRequest) MarshalTo(dAtA []byte) (int, error) {
}
i++
}
- if len(m.Last) > 0 {
- dAtA[i] = 0x1a
+ if m.Seq != 0 {
+ dAtA[i] = 0x20
i++
- i = encodeVarintClient(dAtA, i, uint64(len(m.Last)))
- i += copy(dAtA[i:], m.Last)
+ i = encodeVarintClient(dAtA, i, uint64(m.Seq))
}
- if m.Since != 0 {
- dAtA[i] = 0x20
+ if m.Gen != 0 {
+ dAtA[i] = 0x28
i++
- i = encodeVarintClient(dAtA, i, uint64(m.Since))
+ i = encodeVarintClient(dAtA, i, uint64(m.Gen))
}
- if len(m.Token) > 0 {
- dAtA[i] = 0x2a
+ if len(m.Epoch) > 0 {
+ dAtA[i] = 0x32
i++
- i = encodeVarintClient(dAtA, i, uint64(len(m.Token)))
- i += copy(dAtA[i:], m.Token)
+ i = encodeVarintClient(dAtA, i, uint64(len(m.Epoch)))
+ i += copy(dAtA[i:], m.Epoch)
}
return i, nil
}
@@ -2327,25 +2370,50 @@ func (m *SubscribeResult) MarshalTo(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
- if len(m.Last) > 0 {
- dAtA[i] = 0xa
+ if m.Expires {
+ dAtA[i] = 0x8
+ i++
+ if m.Expires {
+ dAtA[i] = 1
+ } else {
+ dAtA[i] = 0
+ }
i++
- i = encodeVarintClient(dAtA, i, uint64(len(m.Last)))
- i += copy(dAtA[i:], m.Last)
}
- if m.Recovered {
+ if m.TTL != 0 {
dAtA[i] = 0x10
i++
- if m.Recovered {
+ i = encodeVarintClient(dAtA, i, uint64(m.TTL))
+ }
+ if m.Recoverable {
+ dAtA[i] = 0x18
+ i++
+ if m.Recoverable {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i++
}
+ if m.Seq != 0 {
+ dAtA[i] = 0x20
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(m.Seq))
+ }
+ if m.Gen != 0 {
+ dAtA[i] = 0x28
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(m.Gen))
+ }
+ if len(m.Epoch) > 0 {
+ dAtA[i] = 0x32
+ i++
+ i = encodeVarintClient(dAtA, i, uint64(len(m.Epoch)))
+ i += copy(dAtA[i:], m.Epoch)
+ }
if len(m.Publications) > 0 {
for _, msg := range m.Publications {
- dAtA[i] = 0x1a
+ dAtA[i] = 0x3a
i++
i = encodeVarintClient(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
@@ -2355,26 +2423,16 @@ func (m *SubscribeResult) MarshalTo(dAtA []byte) (int, error) {
i += n
}
}
- if m.Expires {
- dAtA[i] = 0x20
+ if m.Recovered {
+ dAtA[i] = 0x40
i++
- if m.Expires {
+ if m.Recovered {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i++
}
- if m.TTL != 0 {
- dAtA[i] = 0x28
- i++
- i = encodeVarintClient(dAtA, i, uint64(m.TTL))
- }
- if m.Time != 0 {
- dAtA[i] = 0x30
- i++
- i = encodeVarintClient(dAtA, i, uint64(m.Time))
- }
return i, nil
}
@@ -2742,11 +2800,6 @@ func (m *PingResult) MarshalTo(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
- if m.Time != 0 {
- dAtA[i] = 0x8
- i++
- i = encodeVarintClient(dAtA, i, uint64(m.Time))
- }
return i, nil
}
@@ -2896,6 +2949,8 @@ func NewPopulatedClientInfo(r randyClient, easy bool) *ClientInfo {
func NewPopulatedPublication(r randyClient, easy bool) *Publication {
this := &Publication{}
+ this.Seq = uint32(r.Uint32())
+ this.Gen = uint32(r.Uint32())
this.UID = string(randStringClient(r))
v6 := NewPopulatedRaw(r)
this.Data = *v6
@@ -2960,7 +3015,6 @@ func NewPopulatedConnectResult(r randyClient, easy bool) *ConnectResult {
this.TTL = uint32(r.Uint32())
v11 := NewPopulatedRaw(r)
this.Data = *v11
- this.Time = uint32(r.Uint32())
if !easy && r.Intn(10) != 0 {
}
return this
@@ -2988,10 +3042,11 @@ func NewPopulatedRefreshResult(r randyClient, easy bool) *RefreshResult {
func NewPopulatedSubscribeRequest(r randyClient, easy bool) *SubscribeRequest {
this := &SubscribeRequest{}
this.Channel = string(randStringClient(r))
- this.Recover = bool(bool(r.Intn(2) == 0))
- this.Last = string(randStringClient(r))
- this.Since = uint32(r.Uint32())
this.Token = string(randStringClient(r))
+ this.Recover = bool(bool(r.Intn(2) == 0))
+ this.Seq = uint32(r.Uint32())
+ this.Gen = uint32(r.Uint32())
+ this.Epoch = string(randStringClient(r))
if !easy && r.Intn(10) != 0 {
}
return this
@@ -2999,8 +3054,12 @@ func NewPopulatedSubscribeRequest(r randyClient, easy bool) *SubscribeRequest {
func NewPopulatedSubscribeResult(r randyClient, easy bool) *SubscribeResult {
this := &SubscribeResult{}
- this.Last = string(randStringClient(r))
- this.Recovered = bool(bool(r.Intn(2) == 0))
+ this.Expires = bool(bool(r.Intn(2) == 0))
+ this.TTL = uint32(r.Uint32())
+ this.Recoverable = bool(bool(r.Intn(2) == 0))
+ this.Seq = uint32(r.Uint32())
+ this.Gen = uint32(r.Uint32())
+ this.Epoch = string(randStringClient(r))
if r.Intn(10) != 0 {
v12 := r.Intn(5)
this.Publications = make([]*Publication, v12)
@@ -3008,9 +3067,7 @@ func NewPopulatedSubscribeResult(r randyClient, easy bool) *SubscribeResult {
this.Publications[i] = NewPopulatedPublication(r, easy)
}
}
- this.Expires = bool(bool(r.Intn(2) == 0))
- this.TTL = uint32(r.Uint32())
- this.Time = uint32(r.Uint32())
+ this.Recovered = bool(bool(r.Intn(2) == 0))
if !easy && r.Intn(10) != 0 {
}
return this
@@ -3136,7 +3193,6 @@ func NewPopulatedPingRequest(r randyClient, easy bool) *PingRequest {
func NewPopulatedPingResult(r randyClient, easy bool) *PingResult {
this := &PingResult{}
- this.Time = uint32(r.Uint32())
if !easy && r.Intn(10) != 0 {
}
return this
@@ -3319,6 +3375,12 @@ func (m *ClientInfo) Size() (n int) {
func (m *Publication) Size() (n int) {
var l int
_ = l
+ if m.Seq != 0 {
+ n += 1 + sovClient(uint64(m.Seq))
+ }
+ if m.Gen != 0 {
+ n += 1 + sovClient(uint64(m.Gen))
+ }
l = len(m.UID)
if l > 0 {
n += 1 + l + sovClient(uint64(l))
@@ -3396,9 +3458,6 @@ func (m *ConnectResult) Size() (n int) {
}
l = m.Data.Size()
n += 1 + l + sovClient(uint64(l))
- if m.Time != 0 {
- n += 1 + sovClient(uint64(m.Time))
- }
return n
}
@@ -3439,17 +3498,20 @@ func (m *SubscribeRequest) Size() (n int) {
if l > 0 {
n += 1 + l + sovClient(uint64(l))
}
+ l = len(m.Token)
+ if l > 0 {
+ n += 1 + l + sovClient(uint64(l))
+ }
if m.Recover {
n += 2
}
- l = len(m.Last)
- if l > 0 {
- n += 1 + l + sovClient(uint64(l))
+ if m.Seq != 0 {
+ n += 1 + sovClient(uint64(m.Seq))
}
- if m.Since != 0 {
- n += 1 + sovClient(uint64(m.Since))
+ if m.Gen != 0 {
+ n += 1 + sovClient(uint64(m.Gen))
}
- l = len(m.Token)
+ l = len(m.Epoch)
if l > 0 {
n += 1 + l + sovClient(uint64(l))
}
@@ -3459,28 +3521,34 @@ func (m *SubscribeRequest) Size() (n int) {
func (m *SubscribeResult) Size() (n int) {
var l int
_ = l
- l = len(m.Last)
- if l > 0 {
- n += 1 + l + sovClient(uint64(l))
+ if m.Expires {
+ n += 2
}
- if m.Recovered {
+ if m.TTL != 0 {
+ n += 1 + sovClient(uint64(m.TTL))
+ }
+ if m.Recoverable {
n += 2
}
+ if m.Seq != 0 {
+ n += 1 + sovClient(uint64(m.Seq))
+ }
+ if m.Gen != 0 {
+ n += 1 + sovClient(uint64(m.Gen))
+ }
+ l = len(m.Epoch)
+ if l > 0 {
+ n += 1 + l + sovClient(uint64(l))
+ }
if len(m.Publications) > 0 {
for _, e := range m.Publications {
l = e.Size()
n += 1 + l + sovClient(uint64(l))
}
}
- if m.Expires {
+ if m.Recovered {
n += 2
}
- if m.TTL != 0 {
- n += 1 + sovClient(uint64(m.TTL))
- }
- if m.Time != 0 {
- n += 1 + sovClient(uint64(m.Time))
- }
return n
}
@@ -3626,9 +3694,6 @@ func (m *PingRequest) Size() (n int) {
func (m *PingResult) Size() (n int) {
var l int
_ = l
- if m.Time != 0 {
- n += 1 + sovClient(uint64(m.Time))
- }
return n
}
@@ -4343,6 +4408,44 @@ func (m *Publication) Unmarshal(dAtA []byte) error {
}
switch fieldNum {
case 1:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Seq", wireType)
+ }
+ m.Seq = 0
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowClient
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ m.Seq |= (uint32(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ case 2:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Gen", wireType)
+ }
+ m.Gen = 0
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowClient
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ m.Gen |= (uint32(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field UID", wireType)
}
@@ -4371,7 +4474,7 @@ func (m *Publication) Unmarshal(dAtA []byte) error {
}
m.UID = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
- case 2:
+ case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
}
@@ -4401,7 +4504,7 @@ func (m *Publication) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
- case 3:
+ case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType)
}
@@ -5030,25 +5133,6 @@ func (m *ConnectResult) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
- case 6:
- if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType)
- }
- m.Time = 0
- for shift := uint(0); ; shift += 7 {
- if shift >= 64 {
- return ErrIntOverflowClient
- }
- if iNdEx >= l {
- return io.ErrUnexpectedEOF
- }
- b := dAtA[iNdEx]
- iNdEx++
- m.Time |= (uint32(b) & 0x7F) << shift
- if b < 0x80 {
- break
- }
- }
default:
iNdEx = preIndex
skippy, err := skipClient(dAtA[iNdEx:])
@@ -5355,6 +5439,35 @@ func (m *SubscribeRequest) Unmarshal(dAtA []byte) error {
m.Channel = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Token", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowClient
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= (uint64(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthClient
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Token = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Recover", wireType)
}
@@ -5374,11 +5487,11 @@ func (m *SubscribeRequest) Unmarshal(dAtA []byte) error {
}
}
m.Recover = bool(v != 0)
- case 3:
- if wireType != 2 {
- return fmt.Errorf("proto: wrong wireType = %d for field Last", wireType)
+ case 4:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Seq", wireType)
}
- var stringLen uint64
+ m.Seq = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5388,26 +5501,16 @@ func (m *SubscribeRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- stringLen |= (uint64(b) & 0x7F) << shift
+ m.Seq |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- intStringLen := int(stringLen)
- if intStringLen < 0 {
- return ErrInvalidLengthClient
- }
- postIndex := iNdEx + intStringLen
- if postIndex > l {
- return io.ErrUnexpectedEOF
- }
- m.Last = string(dAtA[iNdEx:postIndex])
- iNdEx = postIndex
- case 4:
+ case 5:
if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Since", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field Gen", wireType)
}
- m.Since = 0
+ m.Gen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5417,14 +5520,14 @@ func (m *SubscribeRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- m.Since |= (uint32(b) & 0x7F) << shift
+ m.Gen |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- case 5:
+ case 6:
if wireType != 2 {
- return fmt.Errorf("proto: wrong wireType = %d for field Token", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field Epoch", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
@@ -5449,7 +5552,7 @@ func (m *SubscribeRequest) Unmarshal(dAtA []byte) error {
if postIndex > l {
return io.ErrUnexpectedEOF
}
- m.Token = string(dAtA[iNdEx:postIndex])
+ m.Epoch = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
@@ -5502,10 +5605,10 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
switch fieldNum {
case 1:
- if wireType != 2 {
- return fmt.Errorf("proto: wrong wireType = %d for field Last", wireType)
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType)
}
- var stringLen uint64
+ var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5515,26 +5618,17 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- stringLen |= (uint64(b) & 0x7F) << shift
+ v |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- intStringLen := int(stringLen)
- if intStringLen < 0 {
- return ErrInvalidLengthClient
- }
- postIndex := iNdEx + intStringLen
- if postIndex > l {
- return io.ErrUnexpectedEOF
- }
- m.Last = string(dAtA[iNdEx:postIndex])
- iNdEx = postIndex
+ m.Expires = bool(v != 0)
case 2:
if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Recovered", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field TTL", wireType)
}
- var v int
+ m.TTL = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5544,17 +5638,16 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- v |= (int(b) & 0x7F) << shift
+ m.TTL |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- m.Recovered = bool(v != 0)
case 3:
- if wireType != 2 {
- return fmt.Errorf("proto: wrong wireType = %d for field Publications", wireType)
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Recoverable", wireType)
}
- var msglen int
+ var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5564,28 +5657,17 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- msglen |= (int(b) & 0x7F) << shift
+ v |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- if msglen < 0 {
- return ErrInvalidLengthClient
- }
- postIndex := iNdEx + msglen
- if postIndex > l {
- return io.ErrUnexpectedEOF
- }
- m.Publications = append(m.Publications, &Publication{})
- if err := m.Publications[len(m.Publications)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
- return err
- }
- iNdEx = postIndex
+ m.Recoverable = bool(v != 0)
case 4:
if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field Seq", wireType)
}
- var v int
+ m.Seq = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5595,17 +5677,16 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- v |= (int(b) & 0x7F) << shift
+ m.Seq |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
- m.Expires = bool(v != 0)
case 5:
if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field TTL", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field Gen", wireType)
}
- m.TTL = 0
+ m.Gen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5615,16 +5696,76 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- m.TTL |= (uint32(b) & 0x7F) << shift
+ m.Gen |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 6:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Epoch", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowClient
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= (uint64(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthClient
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Epoch = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 7:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Publications", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowClient
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= (int(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthClient
+ }
+ postIndex := iNdEx + msglen
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Publications = append(m.Publications, &Publication{})
+ if err := m.Publications[len(m.Publications)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ case 8:
if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType)
+ return fmt.Errorf("proto: wrong wireType = %d for field Recovered", wireType)
}
- m.Time = 0
+ var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowClient
@@ -5634,11 +5775,12 @@ func (m *SubscribeResult) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
- m.Time |= (uint32(b) & 0x7F) << shift
+ v |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
+ m.Recovered = bool(v != 0)
default:
iNdEx = preIndex
skippy, err := skipClient(dAtA[iNdEx:])
@@ -6803,25 +6945,6 @@ func (m *PingResult) Unmarshal(dAtA []byte) error {
return fmt.Errorf("proto: PingResult: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
- case 1:
- if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType)
- }
- m.Time = 0
- for shift := uint(0); ; shift += 7 {
- if shift >= 64 {
- return ErrIntOverflowClient
- }
- if iNdEx >= l {
- return io.ErrUnexpectedEOF
- }
- b := dAtA[iNdEx]
- iNdEx++
- m.Time |= (uint32(b) & 0x7F) << shift
- if b < 0x80 {
- break
- }
- }
default:
iNdEx = preIndex
skippy, err := skipClient(dAtA[iNdEx:])
@@ -7191,103 +7314,106 @@ var (
func init() { proto1.RegisterFile("client.proto", fileDescriptorClient) }
var fileDescriptorClient = []byte{
- // 1560 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x57, 0xcb, 0x73, 0xdb, 0x44,
- 0x18, 0xcf, 0xfa, 0x11, 0xdb, 0x9f, 0x1f, 0x51, 0x36, 0x4d, 0x9b, 0x9a, 0x60, 0x79, 0x04, 0x49,
- 0x43, 0x06, 0x1a, 0x9a, 0x52, 0x52, 0x28, 0xd0, 0x89, 0x5d, 0xd3, 0xb8, 0x93, 0x38, 0x1e, 0xc9,
- 0x61, 0xa6, 0xc3, 0x21, 0xf8, 0xb1, 0x49, 0x34, 0xb5, 0x25, 0x63, 0xc9, 0x81, 0xfc, 0x07, 0x8c,
- 0x4f, 0x0c, 0x37, 0x0e, 0x3e, 0x30, 0x5c, 0x98, 0xe1, 0xc0, 0x85, 0x19, 0x38, 0x72, 0x2c, 0xb7,
- 0x9e, 0x39, 0x68, 0x20, 0xdc, 0xfc, 0x17, 0x70, 0x64, 0xf6, 0x21, 0x59, 0x72, 0xc9, 0xd4, 0xe9,
- 0x8d, 0x8b, 0xa4, 0xfd, 0xde, 0x8f, 0xdf, 0x7e, 0xbb, 0x82, 0x54, 0xb3, 0xad, 0x13, 0xc3, 0xbe,
- 0xd9, 0xed, 0x99, 0xb6, 0x89, 0xa3, 0xec, 0x95, 0x7d, 0xeb, 0x58, 0xb7, 0x4f, 0xfa, 0x8d, 0x9b,
- 0x4d, 0xb3, 0xb3, 0x71, 0x6c, 0x1e, 0x9b, 0x1b, 0x8c, 0xdc, 0xe8, 0x1f, 0xb1, 0x15, 0x5b, 0xb0,
- 0x2f, 0xae, 0xa5, 0xec, 0x42, 0xb4, 0xd4, 0xeb, 0x99, 0x3d, 0xbc, 0x0c, 0x91, 0xa6, 0xd9, 0x22,
- 0x4b, 0x28, 0x8f, 0xd6, 0xd2, 0x85, 0xf8, 0xc8, 0x91, 0xd9, 0x5a, 0x65, 0x4f, 0xbc, 0x02, 0xb1,
- 0x0e, 0xb1, 0xac, 0xfa, 0x31, 0x59, 0x0a, 0xe5, 0xd1, 0x5a, 0xa2, 0x90, 0x1c, 0x39, 0xb2, 0x4b,
- 0x52, 0xdd, 0x0f, 0xe5, 0x47, 0x04, 0xb1, 0xa2, 0xd9, 0xe9, 0xd4, 0x8d, 0x16, 0x5e, 0x85, 0x90,
- 0xde, 0x12, 0xe6, 0xae, 0x9e, 0x3b, 0x72, 0xa8, 0xfc, 0x60, 0xe4, 0xc8, 0x29, 0xbd, 0xf5, 0xa6,
- 0xd9, 0xd1, 0x6d, 0xd2, 0xe9, 0xda, 0x67, 0x6a, 0x48, 0x6f, 0xe1, 0xfb, 0x30, 0xdb, 0x21, 0xf6,
- 0x89, 0xd9, 0x62, 0x96, 0x33, 0x9b, 0xf3, 0x3c, 0xb2, 0x9b, 0x7b, 0x8c, 0x58, 0x3b, 0xeb, 0x92,
- 0xc2, 0x95, 0x91, 0x23, 0x4b, 0x5c, 0xc8, 0xa7, 0x2c, 0xd4, 0xf0, 0x16, 0xcc, 0x76, 0xeb, 0xbd,
- 0x7a, 0xc7, 0x5a, 0x0a, 0xe7, 0xd1, 0x5a, 0xaa, 0x20, 0x3f, 0x75, 0xe4, 0x99, 0x3f, 0x1c, 0x39,
- 0xac, 0xd6, 0xbf, 0xa0, 0x8a, 0x9c, 0xe9, 0x57, 0xe4, 0x14, 0xe5, 0x3b, 0x04, 0x51, 0x95, 0x74,
- 0xdb, 0x67, 0x53, 0xc7, 0xba, 0x05, 0x51, 0x42, 0xab, 0xc5, 0x42, 0x4d, 0x6e, 0xa6, 0x44, 0xa8,
- 0xac, 0x82, 0x85, 0x85, 0x91, 0x23, 0xcf, 0x31, 0xb6, 0x4f, 0x8b, 0xcb, 0xd3, 0x18, 0x7b, 0xc4,
- 0xea, 0xb7, 0xed, 0x0b, 0x62, 0xe4, 0x4c, 0x7f, 0x8c, 0x9c, 0xa2, 0x7c, 0x8b, 0x20, 0x52, 0xed,
- 0x5b, 0x27, 0x78, 0x0b, 0x22, 0xf6, 0x59, 0x97, 0xf7, 0x27, 0xb3, 0x39, 0x27, 0x3c, 0x53, 0x16,
- 0x2b, 0x11, 0x1e, 0x39, 0x72, 0x86, 0x0a, 0xf8, 0x6c, 0x30, 0x05, 0xbc, 0x01, 0xb1, 0xe6, 0x49,
- 0xdd, 0x30, 0x48, 0x5b, 0xb4, 0x6e, 0x71, 0xe4, 0xc8, 0xf3, 0x82, 0xe4, 0x93, 0x76, 0xa5, 0xf0,
- 0x0d, 0x88, 0xb4, 0xea, 0x76, 0x5d, 0x44, 0xba, 0x10, 0x8c, 0x94, 0xb1, 0x54, 0xf6, 0x54, 0x9e,
- 0x21, 0x80, 0x22, 0x83, 0x60, 0xd9, 0x38, 0x32, 0x29, 0x82, 0xfa, 0x16, 0xe9, 0xb1, 0x08, 0x13,
- 0x1c, 0x41, 0x74, 0xad, 0xb2, 0x27, 0x56, 0x60, 0x96, 0xc3, 0x55, 0x44, 0x01, 0x23, 0x47, 0x16,
- 0x14, 0x55, 0xbc, 0xf1, 0x7d, 0x48, 0x34, 0x4d, 0xc3, 0x38, 0xd4, 0x8d, 0x23, 0x53, 0xb8, 0x57,
- 0x82, 0xee, 0x17, 0x3c, 0xbe, 0x2f, 0xf2, 0x38, 0x25, 0xb2, 0x10, 0xa8, 0x81, 0x93, 0xba, 0x30,
- 0x10, 0xf9, 0x6f, 0x03, 0x2e, 0x3f, 0x60, 0xe0, 0xa4, 0xce, 0x0c, 0x28, 0x43, 0x04, 0xc9, 0x6a,
- 0xbf, 0xd1, 0xd6, 0x9b, 0x75, 0x5b, 0x37, 0x0d, 0xbc, 0x0e, 0xe1, 0xbe, 0x40, 0x46, 0xa2, 0xb0,
- 0x74, 0xee, 0xc8, 0xe1, 0x03, 0x06, 0x8d, 0x74, 0x3f, 0x80, 0x0d, 0x2a, 0xe4, 0xd5, 0x2d, 0xf4,
- 0x82, 0xba, 0xe1, 0xf7, 0x20, 0xe2, 0x65, 0x98, 0xf4, 0xf0, 0x3e, 0xae, 0x24, 0x6f, 0xe6, 0x44,
- 0x8c, 0x4c, 0x45, 0xb9, 0x07, 0x91, 0x47, 0xa6, 0x6e, 0xe0, 0xdb, 0xc2, 0x04, 0xba, 0xc8, 0x44,
- 0x8a, 0xba, 0xa7, 0x7e, 0xa9, 0x98, 0x50, 0xfe, 0x00, 0xa2, 0xbb, 0xa4, 0x7e, 0x4a, 0x5e, 0x4e,
- 0xfb, 0x01, 0x44, 0x0f, 0x0c, 0xab, 0xdf, 0xc0, 0xf7, 0x20, 0x49, 0xc1, 0xd9, 0xb0, 0x9a, 0x3d,
- 0xbd, 0xc1, 0x01, 0x19, 0x2f, 0x5c, 0x1f, 0x39, 0xf2, 0xa2, 0x8f, 0xec, 0x8b, 0xdc, 0x2f, 0xad,
- 0x6c, 0x42, 0x6c, 0x8f, 0x0f, 0x0b, 0xaf, 0x5e, 0xe8, 0x45, 0x38, 0x6b, 0x41, 0xa6, 0x68, 0x1a,
- 0x06, 0x69, 0xda, 0x2a, 0xf9, 0xbc, 0x4f, 0x2c, 0x1b, 0xcb, 0x10, 0xb5, 0xcd, 0x27, 0xc4, 0x10,
- 0x8d, 0x49, 0x8c, 0x1c, 0x99, 0x13, 0x54, 0xfe, 0xc2, 0xb7, 0x02, 0xbd, 0x78, 0x35, 0x68, 0x3b,
- 0x43, 0x59, 0xfe, 0xd2, 0x32, 0x2f, 0xdf, 0x84, 0x20, 0xed, 0xb9, 0xa1, 0x7b, 0xcf, 0x07, 0x59,
- 0x74, 0x21, 0x64, 0x57, 0x20, 0x76, 0x4a, 0x7a, 0x96, 0x6e, 0x1a, 0xfe, 0xc1, 0x28, 0x48, 0xaa,
- 0xfb, 0x41, 0x37, 0x21, 0xf9, 0xb2, 0xab, 0xf7, 0x08, 0x1f, 0x52, 0x71, 0xbe, 0x09, 0x05, 0xc9,
- 0xbf, 0x09, 0x05, 0x89, 0x02, 0xcf, 0xb6, 0xdb, 0x0c, 0xc3, 0x69, 0x0e, 0xbc, 0x5a, 0x6d, 0x97,
- 0x02, 0xcf, 0xb6, 0xfd, 0x9b, 0x96, 0x0a, 0x79, 0xc9, 0x46, 0xa7, 0x4e, 0x16, 0xaf, 0x42, 0xc4,
- 0xd6, 0x3b, 0x64, 0x69, 0x96, 0xd9, 0xe7, 0xc3, 0x43, 0xef, 0x04, 0x87, 0x87, 0xde, 0x21, 0xca,
- 0x2d, 0xc8, 0xa8, 0xe4, 0xa8, 0x47, 0xac, 0x93, 0x69, 0x4b, 0xaf, 0xfc, 0x82, 0x20, 0xed, 0xe9,
- 0xfc, 0x9f, 0xea, 0xa8, 0xfc, 0x86, 0x40, 0xd2, 0x5c, 0xa4, 0xba, 0xf9, 0xae, 0x8c, 0xc7, 0x27,
- 0x1a, 0x07, 0x26, 0x48, 0xe3, 0xa1, 0xb9, 0x02, 0xb1, 0x1e, 0x69, 0x9a, 0xa7, 0x84, 0x9f, 0x0d,
- 0x71, 0x2e, 0x26, 0x48, 0xaa, 0xfb, 0x41, 0x67, 0x64, 0xbb, 0x6e, 0xf1, 0x53, 0x40, 0xcc, 0x48,
- 0xba, 0x56, 0xd9, 0x93, 0xd6, 0xd6, 0xd2, 0x8d, 0x26, 0x11, 0xe1, 0xb2, 0xda, 0x32, 0x82, 0xca,
- 0x5f, 0xe3, 0xe2, 0x47, 0x2f, 0x28, 0xfe, 0xef, 0x21, 0x98, 0xf3, 0xa5, 0xc0, 0xca, 0xbf, 0x2a,
- 0x7c, 0xf2, 0xf0, 0x59, 0xaf, 0xe9, 0xda, 0xdf, 0x6b, 0xe6, 0xfd, 0x0e, 0x24, 0x44, 0x98, 0xa4,
- 0x25, 0x92, 0xb8, 0x46, 0x27, 0xa6, 0x47, 0xf4, 0x69, 0x8c, 0x25, 0xb1, 0x0a, 0xa9, 0xee, 0x78,
- 0x62, 0xd2, 0xbe, 0x84, 0xd7, 0x92, 0x9b, 0xd8, 0x3b, 0xa0, 0x3c, 0x56, 0x21, 0x3b, 0x72, 0xe4,
- 0xab, 0x7e, 0x59, 0x9f, 0xc1, 0x80, 0x0d, 0x7f, 0x9b, 0x23, 0x97, 0x69, 0x73, 0x74, 0x9a, 0xed,
- 0x32, 0x2d, 0xf6, 0x3f, 0x85, 0x79, 0xad, 0xdf, 0x98, 0x80, 0xff, 0x94, 0x70, 0xf0, 0x1a, 0x15,
- 0xba, 0xa0, 0x51, 0x26, 0x83, 0x5a, 0x70, 0x9f, 0xf8, 0xb2, 0x46, 0x97, 0xc9, 0x3a, 0x34, 0x0d,
- 0xb8, 0xef, 0x01, 0x66, 0xe3, 0xfb, 0x65, 0xd0, 0xad, 0x2c, 0xc0, 0x7c, 0x40, 0x99, 0x5d, 0x4d,
- 0x3e, 0x83, 0x0c, 0xeb, 0xee, 0xa5, 0x8b, 0x33, 0xed, 0x41, 0xa9, 0xcc, 0x41, 0xda, 0xf3, 0xc0,
- 0x5c, 0xde, 0x85, 0xb9, 0x6a, 0x8f, 0x58, 0x84, 0x6e, 0x89, 0xcb, 0x65, 0xf0, 0x13, 0x82, 0xcc,
- 0x58, 0x95, 0x95, 0x7b, 0x0f, 0xe2, 0x5d, 0x41, 0x59, 0x42, 0x0c, 0xb4, 0xaf, 0xb9, 0xa0, 0x0d,
- 0x08, 0x7a, 0xcb, 0x92, 0x61, 0xf7, 0xce, 0x0a, 0xa9, 0x91, 0x23, 0x7b, 0x8a, 0xaa, 0xf7, 0x95,
- 0xad, 0x40, 0x3a, 0x20, 0x88, 0x25, 0x08, 0x3f, 0x21, 0x67, 0x3c, 0x2a, 0x95, 0x7e, 0xe2, 0x1b,
- 0x10, 0x3d, 0xad, 0xb7, 0xfb, 0x44, 0x5c, 0x1f, 0x9f, 0x3f, 0x78, 0x55, 0xce, 0x7f, 0x3f, 0x74,
- 0x17, 0x29, 0x1f, 0xc2, 0x15, 0xd7, 0x9e, 0x66, 0xd7, 0x6d, 0xeb, 0x92, 0x09, 0x5b, 0xb0, 0x30,
- 0xa1, 0xce, 0x92, 0x7e, 0x1b, 0x92, 0x46, 0xbf, 0x73, 0xc8, 0xa7, 0xae, 0x25, 0xae, 0xbc, 0x73,
- 0x23, 0x47, 0xf6, 0x93, 0x55, 0x30, 0xfa, 0x1d, 0x1e, 0x15, 0x05, 0x59, 0x82, 0xb2, 0xe8, 0x25,
- 0xce, 0x12, 0x50, 0x4b, 0x8f, 0x1c, 0x79, 0x4c, 0x54, 0xe3, 0x46, 0xbf, 0x73, 0x40, 0xbf, 0x94,
- 0x2d, 0xc8, 0xec, 0xe8, 0x96, 0x6d, 0xf6, 0xce, 0x2e, 0x19, 0xed, 0x63, 0x48, 0x7b, 0x8a, 0x2c,
- 0xce, 0x9d, 0x89, 0xa9, 0x82, 0x2e, 0x9c, 0x2a, 0x12, 0xbd, 0xa9, 0xfb, 0x65, 0x83, 0xb3, 0x44,
- 0x49, 0x43, 0xb2, 0xaa, 0x1b, 0xc7, 0x22, 0x20, 0xe5, 0x1d, 0x00, 0xbe, 0x74, 0x67, 0x23, 0x9b,
- 0x05, 0xe8, 0x05, 0xb3, 0xe0, 0x0e, 0x80, 0x5a, 0x2d, 0xba, 0x49, 0x4d, 0x7d, 0x73, 0xf9, 0x08,
- 0x12, 0x4c, 0x8d, 0xf9, 0xba, 0x15, 0xd0, 0x9a, 0xea, 0x4e, 0xf2, 0x2e, 0x24, 0x35, 0x62, 0xb4,
- 0x2e, 0xeb, 0x77, 0xfd, 0x59, 0x18, 0x60, 0xfc, 0xff, 0x84, 0x15, 0x88, 0x15, 0xf7, 0x2b, 0x95,
- 0x52, 0xb1, 0x26, 0xcd, 0x64, 0x17, 0x07, 0xc3, 0xfc, 0xfc, 0x98, 0x29, 0xae, 0x3c, 0x78, 0x15,
- 0x12, 0xda, 0x41, 0x41, 0x2b, 0xaa, 0xe5, 0x42, 0x49, 0x42, 0xd9, 0x6b, 0x83, 0x61, 0x7e, 0x61,
- 0x2c, 0xe5, 0x9d, 0x29, 0x78, 0x1d, 0x92, 0x07, 0x95, 0xb1, 0x64, 0x28, 0x7b, 0x7d, 0x30, 0xcc,
- 0x2f, 0x8e, 0x25, 0x7d, 0x73, 0x82, 0xfa, 0xad, 0x1e, 0x14, 0x76, 0xcb, 0xda, 0x8e, 0x14, 0x9e,
- 0xf4, 0x2b, 0x36, 0x36, 0x7e, 0x1d, 0xe2, 0x55, 0xb5, 0xa4, 0x95, 0x2a, 0xc5, 0x92, 0x14, 0xc9,
- 0x5e, 0x1d, 0x0c, 0xf3, 0xd8, 0x27, 0x24, 0x10, 0x8c, 0x37, 0x20, 0xe3, 0x4a, 0x1d, 0x6a, 0xb5,
- 0xed, 0x9a, 0x26, 0x45, 0xb3, 0xaf, 0x0c, 0x86, 0xf9, 0x6b, 0xcf, 0xcb, 0x32, 0xb4, 0x53, 0xd7,
- 0x3b, 0x65, 0xad, 0xb6, 0xaf, 0x3e, 0x96, 0x66, 0x27, 0x5d, 0x0b, 0xa4, 0xd1, 0xc3, 0xb8, 0x5a,
- 0xae, 0x3c, 0x94, 0x62, 0x59, 0x3c, 0x18, 0xe6, 0x33, 0x3e, 0x53, 0xba, 0x71, 0x4c, 0xb9, 0x5a,
- 0xa9, 0xf2, 0x40, 0x8a, 0x4f, 0x72, 0x69, 0x47, 0x70, 0x16, 0xc2, 0x6a, 0xb5, 0x28, 0x25, 0xb2,
- 0xf3, 0x83, 0x61, 0x3e, 0x3d, 0x66, 0xaa, 0xd5, 0x22, 0xf5, 0xad, 0x96, 0x3e, 0x56, 0x4b, 0xda,
- 0x8e, 0x04, 0x93, 0xbe, 0xc5, 0xc4, 0xc7, 0x6f, 0x40, 0x52, 0x3b, 0x28, 0x1c, 0xba, 0x72, 0xc9,
- 0xec, 0xd2, 0x60, 0x98, 0xbf, 0x12, 0x28, 0xb8, 0x10, 0xcd, 0x46, 0xbe, 0xfa, 0x3e, 0x37, 0xb3,
- 0xfe, 0x33, 0x82, 0xb8, 0xfb, 0xb7, 0x87, 0xd7, 0x20, 0xc9, 0x0a, 0x5b, 0xdc, 0xae, 0x95, 0xf7,
- 0x2b, 0xd2, 0x0c, 0x6f, 0x97, 0xcb, 0xf6, 0xff, 0xc0, 0x64, 0x21, 0xf2, 0x68, 0xbf, 0x5c, 0x91,
- 0x50, 0x56, 0x1a, 0x0c, 0xf3, 0x29, 0x57, 0x84, 0xfd, 0x44, 0x2c, 0x43, 0x74, 0xb7, 0xb4, 0xfd,
- 0x09, 0x6d, 0x22, 0xcb, 0xc2, 0x65, 0xf2, 0x9f, 0x84, 0x65, 0x88, 0xb2, 0x46, 0x4b, 0xe1, 0x20,
- 0x97, 0xff, 0x04, 0xe4, 0x21, 0xb6, 0x57, 0xd2, 0xb4, 0xed, 0x87, 0xb4, 0x6b, 0x0b, 0x83, 0x61,
- 0x7e, 0xce, 0xe5, 0x8b, 0xeb, 0x3d, 0x0f, 0xbb, 0xb0, 0xfc, 0xcf, 0x5f, 0x39, 0xf4, 0xc3, 0x79,
- 0x0e, 0xfd, 0x7a, 0x9e, 0x43, 0x4f, 0xcf, 0x73, 0xe8, 0xd9, 0x79, 0x0e, 0xfd, 0x79, 0x9e, 0x43,
- 0x5f, 0xff, 0x9d, 0x9b, 0x69, 0xcc, 0xb2, 0xed, 0x7c, 0xfb, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff,
- 0x1b, 0x48, 0xfe, 0x37, 0xca, 0x10, 0x00, 0x00,
+ // 1610 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x57, 0x4b, 0x6f, 0x23, 0x59,
+ 0x15, 0x4e, 0xd9, 0xae, 0xd8, 0x3e, 0x7e, 0xa4, 0x72, 0xd3, 0xe9, 0x76, 0x4c, 0x70, 0x59, 0x35,
+ 0x64, 0x3a, 0x13, 0x41, 0x87, 0xce, 0x68, 0xc8, 0x40, 0x03, 0xa3, 0xd8, 0x6d, 0x26, 0x1e, 0x25,
+ 0x8e, 0x55, 0xe5, 0x20, 0x8d, 0x58, 0x04, 0x3f, 0x6e, 0xec, 0x52, 0xdb, 0x55, 0xee, 0xaa, 0x72,
+ 0x20, 0xff, 0x00, 0x79, 0xc5, 0x96, 0x85, 0x17, 0x88, 0x0d, 0x52, 0x2f, 0xd8, 0x20, 0xc1, 0x4f,
+ 0xe8, 0x65, 0x2f, 0x11, 0x8b, 0x12, 0x84, 0x05, 0x52, 0xfd, 0x02, 0x96, 0xe8, 0x3e, 0xea, 0xe5,
+ 0x26, 0xdd, 0x49, 0x0b, 0x16, 0x6c, 0xec, 0x7b, 0xcf, 0xfb, 0x9c, 0xfb, 0x9d, 0x73, 0xeb, 0x42,
+ 0xbe, 0x3f, 0xd6, 0xb1, 0xe1, 0x3c, 0x99, 0x5a, 0xa6, 0x63, 0x22, 0x91, 0xfe, 0x95, 0xbf, 0x33,
+ 0xd4, 0x9d, 0xd1, 0xac, 0xf7, 0xa4, 0x6f, 0x4e, 0xf6, 0x87, 0xe6, 0xd0, 0xdc, 0xa7, 0xe4, 0xde,
+ 0xec, 0x92, 0xee, 0xe8, 0x86, 0xae, 0x98, 0x96, 0x72, 0x02, 0x62, 0xc3, 0xb2, 0x4c, 0x0b, 0x6d,
+ 0x43, 0xaa, 0x6f, 0x0e, 0x70, 0x49, 0xa8, 0x0a, 0xbb, 0x85, 0x5a, 0xc6, 0x73, 0x65, 0xba, 0x57,
+ 0xe9, 0x2f, 0xda, 0x81, 0xf4, 0x04, 0xdb, 0x76, 0x77, 0x88, 0x4b, 0x89, 0xaa, 0xb0, 0x9b, 0xad,
+ 0xe5, 0x3c, 0x57, 0xf6, 0x49, 0xaa, 0xbf, 0x50, 0x5e, 0x09, 0x90, 0xae, 0x9b, 0x93, 0x49, 0xd7,
+ 0x18, 0xa0, 0x8f, 0x21, 0xa1, 0x0f, 0xb8, 0xb9, 0x87, 0x37, 0xae, 0x9c, 0x68, 0x3e, 0xf7, 0x5c,
+ 0x39, 0xaf, 0x0f, 0xbe, 0x6d, 0x4e, 0x74, 0x07, 0x4f, 0xa6, 0xce, 0xb5, 0x9a, 0xd0, 0x07, 0xe8,
+ 0x0b, 0x58, 0x9d, 0x60, 0x67, 0x64, 0x0e, 0xa8, 0xe5, 0xe2, 0xc1, 0x3a, 0x8b, 0xec, 0xc9, 0x29,
+ 0x25, 0x76, 0xae, 0xa7, 0xb8, 0xf6, 0xc0, 0x73, 0x65, 0x89, 0x09, 0x45, 0x94, 0xb9, 0x1a, 0x3a,
+ 0x84, 0xd5, 0x69, 0xd7, 0xea, 0x4e, 0xec, 0x52, 0xb2, 0x2a, 0xec, 0xe6, 0x6b, 0xf2, 0x6b, 0x57,
+ 0x5e, 0xf9, 0xab, 0x2b, 0x27, 0xd5, 0xee, 0x2f, 0x88, 0x22, 0x63, 0x46, 0x15, 0x19, 0x45, 0xf9,
+ 0xad, 0x00, 0xa2, 0x8a, 0xa7, 0xe3, 0xeb, 0x3b, 0xc7, 0x7a, 0x08, 0x22, 0x26, 0xd5, 0xa2, 0xa1,
+ 0xe6, 0x0e, 0xf2, 0x3c, 0x54, 0x5a, 0xc1, 0xda, 0x86, 0xe7, 0xca, 0x6b, 0x94, 0x1d, 0xd1, 0x62,
+ 0xf2, 0x24, 0x46, 0x0b, 0xdb, 0xb3, 0xb1, 0x73, 0x4b, 0x8c, 0x8c, 0x19, 0x8d, 0x91, 0x51, 0x94,
+ 0xdf, 0x08, 0x90, 0x6a, 0xcf, 0xec, 0x11, 0x3a, 0x84, 0x94, 0x73, 0x3d, 0x65, 0xe7, 0x53, 0x3c,
+ 0x58, 0xe3, 0x9e, 0x09, 0x8b, 0x96, 0x08, 0x79, 0xae, 0x5c, 0x24, 0x02, 0x11, 0x1b, 0x54, 0x01,
+ 0xed, 0x43, 0xba, 0x3f, 0xea, 0x1a, 0x06, 0x1e, 0xf3, 0xa3, 0xdb, 0xf4, 0x5c, 0x79, 0x9d, 0x93,
+ 0x22, 0xd2, 0xbe, 0x14, 0x7a, 0x0c, 0xa9, 0x41, 0xd7, 0xe9, 0xf2, 0x48, 0x37, 0xe2, 0x91, 0x52,
+ 0x96, 0x4a, 0x7f, 0x95, 0x37, 0x02, 0x40, 0x9d, 0x42, 0xb0, 0x69, 0x5c, 0x9a, 0x04, 0x41, 0x33,
+ 0x1b, 0x5b, 0x34, 0xc2, 0x2c, 0x43, 0x10, 0xd9, 0xab, 0xf4, 0x17, 0x29, 0xb0, 0xca, 0xe0, 0xca,
+ 0xa3, 0x00, 0xcf, 0x95, 0x39, 0x45, 0xe5, 0xff, 0xe8, 0x0b, 0xc8, 0xf6, 0x4d, 0xc3, 0xb8, 0xd0,
+ 0x8d, 0x4b, 0x93, 0xbb, 0x57, 0xe2, 0xee, 0x37, 0x02, 0x7e, 0x24, 0xf2, 0x0c, 0x21, 0xd2, 0x10,
+ 0x88, 0x81, 0x51, 0x97, 0x1b, 0x48, 0xfd, 0x67, 0x03, 0x3e, 0x3f, 0x66, 0x60, 0xd4, 0xa5, 0x06,
+ 0x94, 0x7f, 0x0a, 0x90, 0x6b, 0xcf, 0x7a, 0x63, 0xbd, 0xdf, 0x75, 0x74, 0xd3, 0x40, 0x1f, 0x41,
+ 0xd2, 0xc6, 0x2f, 0x39, 0x32, 0xd6, 0x3d, 0x57, 0x2e, 0xd8, 0xf8, 0x65, 0x44, 0x93, 0x70, 0x89,
+ 0xd0, 0x10, 0x1b, 0x34, 0x2f, 0x2e, 0x34, 0xc4, 0x46, 0x54, 0x68, 0x88, 0x0d, 0xb4, 0x07, 0xc9,
+ 0x99, 0x3e, 0xa0, 0x59, 0x65, 0x6b, 0xa5, 0x1b, 0x57, 0x4e, 0x9e, 0x53, 0x90, 0x15, 0x66, 0x31,
+ 0x94, 0x11, 0xa1, 0xe0, 0x04, 0x52, 0xef, 0x39, 0x01, 0xf4, 0x7d, 0x48, 0xd1, 0x54, 0x45, 0x0a,
+ 0x47, 0xbf, 0x73, 0xc2, 0x33, 0x61, 0xb0, 0x58, 0xca, 0x96, 0xaa, 0x28, 0xcf, 0x20, 0xf5, 0x95,
+ 0xa9, 0x1b, 0xe8, 0x53, 0x6e, 0x42, 0xb8, 0xcd, 0x44, 0x9e, 0xb8, 0x27, 0x7e, 0x89, 0x18, 0x57,
+ 0xfe, 0x21, 0x88, 0x27, 0xb8, 0x7b, 0x85, 0x3f, 0x4c, 0xfb, 0x39, 0x88, 0xe7, 0x86, 0x3d, 0xeb,
+ 0xa1, 0x67, 0x90, 0x23, 0x30, 0xef, 0xd9, 0x7d, 0x4b, 0xef, 0x31, 0x68, 0x67, 0x6a, 0x5b, 0x9e,
+ 0x2b, 0x6f, 0x46, 0xc8, 0x91, 0xc8, 0xa3, 0xd2, 0xca, 0x01, 0xa4, 0x4f, 0xd9, 0xd8, 0x09, 0xea,
+ 0x25, 0xbc, 0x0f, 0xb1, 0x03, 0x28, 0xd6, 0x4d, 0xc3, 0xc0, 0x7d, 0x47, 0xc5, 0x2f, 0x67, 0xd8,
+ 0x76, 0x90, 0x0c, 0xa2, 0x63, 0xbe, 0xc0, 0x06, 0x47, 0x6d, 0xd6, 0x73, 0x65, 0x46, 0x50, 0xd9,
+ 0x1f, 0x7a, 0xca, 0x6d, 0x27, 0xa8, 0xed, 0x6f, 0xc6, 0x6d, 0x17, 0x09, 0x2b, 0x5a, 0x5a, 0xea,
+ 0xc5, 0x13, 0xa0, 0x10, 0xb8, 0x21, 0x5d, 0x1c, 0x01, 0xbf, 0x70, 0x2b, 0xf8, 0x77, 0x20, 0x7d,
+ 0x85, 0x2d, 0x5b, 0x37, 0x8d, 0xe8, 0x88, 0xe5, 0x24, 0xd5, 0x5f, 0x90, 0x76, 0xc6, 0xbf, 0x9c,
+ 0xea, 0x16, 0x66, 0xe3, 0x2e, 0xc3, 0xda, 0x99, 0x93, 0xa2, 0xed, 0xcc, 0x49, 0x04, 0x78, 0x8e,
+ 0x33, 0xa6, 0x58, 0x2a, 0x30, 0xe0, 0x75, 0x3a, 0x27, 0x04, 0x78, 0x8e, 0x13, 0x6d, 0x7f, 0x22,
+ 0x14, 0x24, 0x2b, 0xde, 0x3d, 0xd9, 0xa7, 0x50, 0x54, 0xf1, 0xa5, 0x85, 0xed, 0xd1, 0x5d, 0x4b,
+ 0xaa, 0xfc, 0x49, 0x80, 0x42, 0xa0, 0xf3, 0xff, 0x54, 0x1f, 0xe5, 0x2f, 0x02, 0x48, 0x9a, 0x8f,
+ 0x40, 0x3f, 0xdf, 0x9d, 0x70, 0xc0, 0x0a, 0x61, 0x60, 0x9c, 0x14, 0x8e, 0xd5, 0xa0, 0x2c, 0x89,
+ 0x5b, 0x90, 0xb6, 0x03, 0x69, 0x0b, 0xf7, 0xcd, 0x2b, 0x6c, 0xf1, 0xc8, 0xa9, 0x1d, 0x4e, 0x52,
+ 0xfd, 0x05, 0xda, 0x62, 0x23, 0x89, 0xc5, 0x9b, 0xf6, 0x5c, 0x99, 0x6c, 0xd9, 0x20, 0xda, 0x62,
+ 0x83, 0x48, 0x0c, 0x59, 0x43, 0x6c, 0xb0, 0xf1, 0x23, 0x83, 0x88, 0xa7, 0x66, 0x7f, 0x54, 0x5a,
+ 0x0d, 0xbd, 0x53, 0x82, 0xca, 0xfe, 0x94, 0x57, 0x49, 0x58, 0x8b, 0xa4, 0x46, 0x8f, 0x25, 0x52,
+ 0x4b, 0xe1, 0x3e, 0xb5, 0x4c, 0xdc, 0x05, 0x6b, 0xb4, 0xf9, 0x69, 0x4a, 0xdd, 0xde, 0x18, 0xf3,
+ 0x94, 0x79, 0xf3, 0x07, 0xe4, 0x78, 0xf3, 0x07, 0x64, 0x7f, 0x2e, 0xa7, 0xee, 0x32, 0x97, 0xc5,
+ 0x77, 0xce, 0xe5, 0x4f, 0xe2, 0x85, 0x61, 0x97, 0x38, 0x21, 0xc4, 0x2e, 0x71, 0x42, 0x40, 0x2a,
+ 0xe4, 0xa7, 0xe1, 0xdd, 0x60, 0x97, 0xd2, 0xd5, 0xe4, 0x6e, 0xee, 0x00, 0x05, 0x57, 0x71, 0xc0,
+ 0xaa, 0x95, 0x3d, 0x57, 0x7e, 0x18, 0x95, 0x8d, 0x18, 0x8b, 0xd9, 0x40, 0x9f, 0x41, 0x96, 0xe7,
+ 0x85, 0x07, 0xa5, 0x0c, 0xad, 0xc1, 0x23, 0x72, 0x4d, 0x05, 0xc4, 0x88, 0x66, 0x28, 0xa9, 0xfc,
+ 0x0c, 0xd6, 0xb5, 0x59, 0x6f, 0xa9, 0xf1, 0xfe, 0x4b, 0x40, 0x54, 0x4c, 0x0a, 0xf2, 0x78, 0x87,
+ 0xfe, 0x2f, 0xa1, 0xa0, 0x3c, 0x03, 0x44, 0x2f, 0x84, 0x0f, 0xe9, 0x2b, 0x65, 0x03, 0xd6, 0x63,
+ 0xca, 0xf4, 0xb3, 0xe9, 0xe7, 0x50, 0xa4, 0xe7, 0x71, 0xef, 0xe2, 0x3c, 0x8e, 0x8d, 0xfb, 0x77,
+ 0x5c, 0x25, 0x6b, 0x50, 0x08, 0x3c, 0x50, 0x97, 0x9f, 0xc3, 0x5a, 0xdb, 0xc2, 0x36, 0x36, 0xfa,
+ 0xf7, 0xcd, 0xe0, 0x0f, 0x02, 0x14, 0x43, 0x55, 0x5a, 0xee, 0x53, 0xc8, 0x4c, 0x39, 0xa5, 0x24,
+ 0x50, 0x98, 0x7d, 0xe4, 0xc3, 0x2c, 0x26, 0x18, 0x6c, 0x1b, 0x86, 0x63, 0x5d, 0xd7, 0xf2, 0x9e,
+ 0x2b, 0x07, 0x8a, 0x6a, 0xb0, 0x2a, 0xb7, 0xa0, 0x10, 0x13, 0x44, 0x12, 0x24, 0x5f, 0xe0, 0x6b,
+ 0x16, 0x95, 0x4a, 0x96, 0xe8, 0x31, 0x88, 0x57, 0xdd, 0xf1, 0x0c, 0xf3, 0x4f, 0xdb, 0xb7, 0xaf,
+ 0x72, 0x95, 0xf1, 0x7f, 0x90, 0xf8, 0x5c, 0x50, 0x7e, 0x04, 0x0f, 0x7c, 0x7b, 0x9a, 0xd3, 0x75,
+ 0xec, 0x7b, 0x26, 0x6c, 0xc3, 0xc6, 0x92, 0x3a, 0x4d, 0xfa, 0xbb, 0x90, 0x33, 0x66, 0x93, 0x0b,
+ 0x36, 0xef, 0x6d, 0xfe, 0xd1, 0xb5, 0xe6, 0xb9, 0x72, 0x94, 0xac, 0x82, 0x31, 0x9b, 0xb0, 0xa8,
+ 0x08, 0xc8, 0xb2, 0x84, 0x45, 0x3e, 0x30, 0x6d, 0x0e, 0xb5, 0x82, 0xe7, 0xca, 0x21, 0x51, 0xcd,
+ 0x18, 0xb3, 0xc9, 0x39, 0x59, 0x29, 0x87, 0x50, 0x3c, 0xd6, 0x6d, 0xc7, 0xb4, 0xae, 0xef, 0x19,
+ 0xed, 0xd7, 0x50, 0x08, 0x14, 0x69, 0x9c, 0xc7, 0x4b, 0x73, 0x40, 0xb8, 0x75, 0x0e, 0x48, 0xe4,
+ 0x15, 0x11, 0x95, 0x8d, 0x77, 0xbf, 0x52, 0x80, 0x5c, 0x5b, 0x37, 0x86, 0x3c, 0x20, 0x25, 0x0f,
+ 0xc0, 0xb6, 0x14, 0x50, 0x9f, 0x01, 0xa8, 0xed, 0xba, 0x1f, 0xec, 0x9d, 0xbf, 0x71, 0x7e, 0x0c,
+ 0x59, 0xaa, 0x46, 0x43, 0x7d, 0x1a, 0xd3, 0xba, 0xd3, 0x85, 0xfe, 0x3d, 0xc8, 0x69, 0xd8, 0x18,
+ 0xdc, 0xd7, 0xef, 0xde, 0x9b, 0x24, 0x40, 0xf8, 0x66, 0x43, 0x0a, 0xa4, 0xeb, 0x67, 0xad, 0x56,
+ 0xa3, 0xde, 0x91, 0x56, 0xca, 0x9b, 0xf3, 0x45, 0x75, 0x3d, 0x64, 0xf2, 0x8f, 0x23, 0xf4, 0x31,
+ 0x64, 0xb5, 0xf3, 0x9a, 0x56, 0x57, 0x9b, 0xb5, 0x86, 0x24, 0x94, 0x1f, 0xcd, 0x17, 0xd5, 0x8d,
+ 0x50, 0x2a, 0xb8, 0x8d, 0xd0, 0x1e, 0xe4, 0xce, 0x5b, 0xa1, 0x64, 0xa2, 0xbc, 0x35, 0x5f, 0x54,
+ 0x37, 0x43, 0xc9, 0x48, 0xff, 0x13, 0xbf, 0xed, 0xf3, 0xda, 0x49, 0x53, 0x3b, 0x96, 0x92, 0xcb,
+ 0x7e, 0x79, 0xc3, 0xa2, 0x6f, 0x41, 0xa6, 0xad, 0x36, 0xb4, 0x46, 0xab, 0xde, 0x90, 0x52, 0xe5,
+ 0x87, 0xf3, 0x45, 0x15, 0x45, 0x84, 0x38, 0x32, 0xd1, 0x3e, 0x14, 0x7d, 0xa9, 0x0b, 0xad, 0x73,
+ 0xd4, 0xd1, 0x24, 0xb1, 0xfc, 0x8d, 0xf9, 0xa2, 0xfa, 0xe8, 0x6d, 0x59, 0x8a, 0x62, 0xe2, 0xfa,
+ 0xb8, 0xa9, 0x75, 0xce, 0xd4, 0xaf, 0xa5, 0xd5, 0x65, 0xd7, 0x1c, 0x41, 0xe4, 0x91, 0xd4, 0x6e,
+ 0xb6, 0xbe, 0x94, 0xd2, 0x65, 0x34, 0x5f, 0x54, 0x8b, 0x11, 0x53, 0xba, 0x31, 0x24, 0x5c, 0xad,
+ 0xd1, 0x7a, 0x2e, 0x65, 0x96, 0xb9, 0xe4, 0x44, 0x50, 0x19, 0x92, 0x6a, 0xbb, 0x2e, 0x65, 0xcb,
+ 0xeb, 0xf3, 0x45, 0xb5, 0x10, 0x32, 0xd5, 0x76, 0x9d, 0xf8, 0x56, 0x1b, 0x3f, 0x51, 0x1b, 0xda,
+ 0xb1, 0x04, 0xcb, 0xbe, 0xf9, 0x24, 0x47, 0x9f, 0x40, 0x4e, 0x3b, 0xaf, 0x5d, 0xf8, 0x72, 0xb9,
+ 0x72, 0x69, 0xbe, 0xa8, 0x3e, 0x88, 0x15, 0x9c, 0x8b, 0x96, 0x53, 0xbf, 0xfa, 0x5d, 0x65, 0x65,
+ 0xef, 0x8f, 0x02, 0x64, 0xfc, 0x17, 0x26, 0xda, 0x85, 0x1c, 0x2d, 0x6c, 0xfd, 0xa8, 0xd3, 0x3c,
+ 0x6b, 0x49, 0x2b, 0xec, 0xb8, 0x7c, 0x76, 0xf4, 0xd1, 0x54, 0x86, 0xd4, 0x57, 0x67, 0xcd, 0x96,
+ 0x24, 0x94, 0xa5, 0xf9, 0xa2, 0x9a, 0xf7, 0x45, 0xe8, 0x73, 0x63, 0x1b, 0xc4, 0x93, 0xc6, 0xd1,
+ 0x4f, 0xc9, 0x21, 0xd2, 0x2c, 0x7c, 0x26, 0x7b, 0x4e, 0x6c, 0x83, 0x48, 0x0f, 0x5a, 0x4a, 0xc6,
+ 0xb9, 0xec, 0xb9, 0x50, 0x85, 0xf4, 0x69, 0x43, 0xd3, 0x8e, 0xbe, 0x24, 0xa7, 0xb6, 0x31, 0x5f,
+ 0x54, 0xd7, 0x7c, 0x3e, 0x7f, 0x08, 0xb0, 0xb0, 0x6b, 0xdb, 0xff, 0xfa, 0x7b, 0x45, 0xf8, 0xfd,
+ 0x4d, 0x45, 0xf8, 0xf3, 0x4d, 0x45, 0x78, 0x7d, 0x53, 0x11, 0xde, 0xdc, 0x54, 0x84, 0xbf, 0xdd,
+ 0x54, 0x84, 0x5f, 0xff, 0xa3, 0xb2, 0xd2, 0x5b, 0xa5, 0x6d, 0xfa, 0xe9, 0xbf, 0x03, 0x00, 0x00,
+ 0xff, 0xff, 0x72, 0x81, 0x68, 0xfe, 0x3e, 0x11, 0x00, 0x00,
}
diff --git a/internal/proto/client.proto b/internal/proto/client.proto
index 5e8e805d..aae900e7 100644
--- a/internal/proto/client.proto
+++ b/internal/proto/client.proto
@@ -68,9 +68,11 @@ message ClientInfo {
}
message Publication {
- string uid = 1 [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"];
- bytes data = 2 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false];
- ClientInfo info = 3 [(gogoproto.jsontag) = "info,omitempty"];
+ uint32 seq = 1 [(gogoproto.jsontag) = "seq,omitempty"];
+ uint32 gen = 2 [(gogoproto.jsontag) = "gen,omitempty"];
+ string uid = 3 [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"];
+ bytes data = 4 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false];
+ ClientInfo info = 5 [(gogoproto.jsontag) = "info,omitempty"];
}
message Join {
@@ -100,7 +102,6 @@ message ConnectResult {
bool expires = 3 [(gogoproto.jsontag) = "expires,omitempty"];
uint32 ttl = 4 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
bytes data = 5 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data,omitempty", (gogoproto.nullable) = false];
- uint32 time = 6 [(gogoproto.jsontag) = "time,omitempty"];
}
message RefreshRequest {
@@ -116,19 +117,22 @@ message RefreshResult {
message SubscribeRequest {
string channel = 1 [(gogoproto.jsontag) = "channel"];
- bool recover = 2 [(gogoproto.jsontag) = "recover"];
- string last = 3 [(gogoproto.jsontag) = "last"];
- uint32 since = 4 [(gogoproto.jsontag) = "since"];
- string token = 5 [(gogoproto.jsontag) = "token"];
+ string token = 2 [(gogoproto.jsontag) = "token"];
+ bool recover = 3 [(gogoproto.jsontag) = "recover"];
+ uint32 seq = 4 [(gogoproto.jsontag) = "seq"];
+ uint32 gen = 5 [(gogoproto.jsontag) = "gen"];
+ string epoch = 6 [(gogoproto.jsontag) = "epoch"];
}
message SubscribeResult {
- string last = 1 [(gogoproto.jsontag) = "last,omitempty"];
- bool recovered = 2 [(gogoproto.jsontag) = "recovered,omitempty"];
- repeated Publication publications = 3 [(gogoproto.jsontag) = "publications,omitempty"];
- bool expires = 4 [(gogoproto.jsontag) = "expires,omitempty"];
- uint32 ttl = 5 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
- uint32 time = 6 [(gogoproto.jsontag) = "time,omitempty"];
+ bool expires = 1 [(gogoproto.jsontag) = "expires,omitempty"];
+ uint32 ttl = 2 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
+ bool recoverable = 3 [(gogoproto.jsontag) = "recoverable,omitempty"];
+ uint32 seq = 4 [(gogoproto.jsontag) = "seq,omitempty"];
+ uint32 gen = 5 [(gogoproto.jsontag) = "gen,omitempty"];
+ string epoch = 6 [(gogoproto.jsontag) = "epoch,omitempty"];
+ repeated Publication publications = 7 [(gogoproto.jsontag) = "publications,omitempty"];
+ bool recovered = 8 [(gogoproto.jsontag) = "recovered,omitempty"];
}
message SubRefreshRequest {
@@ -183,7 +187,6 @@ message PingRequest {
}
message PingResult {
- uint32 time = 1 [(gogoproto.jsontag) = "time,omitempty"];
}
message RPCRequest{
diff --git a/misc/proto/client.gogo.proto b/misc/proto/client.gogo.proto
index 5e8e805d..aae900e7 100644
--- a/misc/proto/client.gogo.proto
+++ b/misc/proto/client.gogo.proto
@@ -68,9 +68,11 @@ message ClientInfo {
}
message Publication {
- string uid = 1 [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"];
- bytes data = 2 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false];
- ClientInfo info = 3 [(gogoproto.jsontag) = "info,omitempty"];
+ uint32 seq = 1 [(gogoproto.jsontag) = "seq,omitempty"];
+ uint32 gen = 2 [(gogoproto.jsontag) = "gen,omitempty"];
+ string uid = 3 [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"];
+ bytes data = 4 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false];
+ ClientInfo info = 5 [(gogoproto.jsontag) = "info,omitempty"];
}
message Join {
@@ -100,7 +102,6 @@ message ConnectResult {
bool expires = 3 [(gogoproto.jsontag) = "expires,omitempty"];
uint32 ttl = 4 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
bytes data = 5 [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data,omitempty", (gogoproto.nullable) = false];
- uint32 time = 6 [(gogoproto.jsontag) = "time,omitempty"];
}
message RefreshRequest {
@@ -116,19 +117,22 @@ message RefreshResult {
message SubscribeRequest {
string channel = 1 [(gogoproto.jsontag) = "channel"];
- bool recover = 2 [(gogoproto.jsontag) = "recover"];
- string last = 3 [(gogoproto.jsontag) = "last"];
- uint32 since = 4 [(gogoproto.jsontag) = "since"];
- string token = 5 [(gogoproto.jsontag) = "token"];
+ string token = 2 [(gogoproto.jsontag) = "token"];
+ bool recover = 3 [(gogoproto.jsontag) = "recover"];
+ uint32 seq = 4 [(gogoproto.jsontag) = "seq"];
+ uint32 gen = 5 [(gogoproto.jsontag) = "gen"];
+ string epoch = 6 [(gogoproto.jsontag) = "epoch"];
}
message SubscribeResult {
- string last = 1 [(gogoproto.jsontag) = "last,omitempty"];
- bool recovered = 2 [(gogoproto.jsontag) = "recovered,omitempty"];
- repeated Publication publications = 3 [(gogoproto.jsontag) = "publications,omitempty"];
- bool expires = 4 [(gogoproto.jsontag) = "expires,omitempty"];
- uint32 ttl = 5 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
- uint32 time = 6 [(gogoproto.jsontag) = "time,omitempty"];
+ bool expires = 1 [(gogoproto.jsontag) = "expires,omitempty"];
+ uint32 ttl = 2 [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"];
+ bool recoverable = 3 [(gogoproto.jsontag) = "recoverable,omitempty"];
+ uint32 seq = 4 [(gogoproto.jsontag) = "seq,omitempty"];
+ uint32 gen = 5 [(gogoproto.jsontag) = "gen,omitempty"];
+ string epoch = 6 [(gogoproto.jsontag) = "epoch,omitempty"];
+ repeated Publication publications = 7 [(gogoproto.jsontag) = "publications,omitempty"];
+ bool recovered = 8 [(gogoproto.jsontag) = "recovered,omitempty"];
}
message SubRefreshRequest {
@@ -183,7 +187,6 @@ message PingRequest {
}
message PingResult {
- uint32 time = 1 [(gogoproto.jsontag) = "time,omitempty"];
}
message RPCRequest{
diff --git a/misc/proto/client.proto b/misc/proto/client.proto
index 304750f5..0d96b6c4 100644
--- a/misc/proto/client.proto
+++ b/misc/proto/client.proto
@@ -59,9 +59,11 @@ message ClientInfo {
}
message Publication {
- string uid = 1;
- bytes data = 2;
- ClientInfo info = 3;
+ uint32 seq = 1;
+ uint32 gen = 2;
+ string uid = 3;
+ bytes data = 4;
+ ClientInfo info = 5;
}
message Join {
@@ -91,7 +93,6 @@ message ConnectResult {
bool expires = 3;
uint32 ttl = 4;
bytes data = 5;
- uint32 time = 6;
}
message RefreshRequest {
@@ -107,19 +108,22 @@ message RefreshResult {
message SubscribeRequest {
string channel = 1;
- bool recover = 2;
- string last = 3;
- uint32 since = 4;
- string token = 5 ;
+ string token = 2;
+ bool recover = 3;
+ uint32 seq = 4;
+ uint32 gen = 5;
+ string epoch = 6;
}
message SubscribeResult {
- string last = 1;
- bool recovered = 2;
- repeated Publication publications = 3;
- bool expires = 4;
- uint32 ttl = 5;
- uint32 time = 6;
+ bool expires = 1;
+ uint32 ttl = 2;
+ bool recoverable = 3;
+ uint32 seq = 4;
+ uint32 gen = 5;
+ string epoch = 6;
+ repeated Publication publications = 7;
+ bool recovered = 8;
}
message SubRefreshRequest {
@@ -174,7 +178,6 @@ message PingRequest {
}
message PingResult {
- uint32 time = 1;
}
message RPCRequest{
diff --git a/misc/proto/client.template.proto b/misc/proto/client.template.proto
index a4313781..2f1af817 100644
--- a/misc/proto/client.template.proto
+++ b/misc/proto/client.template.proto
@@ -68,9 +68,11 @@ message ClientInfo {
}
message Publication {
- string uid = 1{{if env.Getenv "GOGO"}} [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"]{{end}};
- bytes data = 2{{if env.Getenv "GOGO"}} [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false]{{end}};
- ClientInfo info = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "info,omitempty"]{{end}};
+ uint32 seq = 1{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "seq,omitempty"]{{end}};
+ uint32 gen = 2{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "gen,omitempty"]{{end}};
+ string uid = 3{{if env.Getenv "GOGO"}} [(gogoproto.customname) = "UID", (gogoproto.jsontag) = "uid,omitempty"]{{end}};
+ bytes data = 4{{if env.Getenv "GOGO"}} [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data", (gogoproto.nullable) = false]{{end}};
+ ClientInfo info = 5{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "info,omitempty"]{{end}};
}
message Join {
@@ -100,7 +102,6 @@ message ConnectResult {
bool expires = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "expires,omitempty"]{{end}};
uint32 ttl = 4{{if env.Getenv "GOGO"}} [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"]{{end}};
bytes data = 5{{if env.Getenv "GOGO"}} [(gogoproto.customtype) = "Raw", (gogoproto.jsontag) = "data,omitempty", (gogoproto.nullable) = false]{{end}};
- uint32 time = 6{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "time,omitempty"]{{end}};
}
message RefreshRequest {
@@ -116,19 +117,22 @@ message RefreshResult {
message SubscribeRequest {
string channel = 1{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "channel"]{{end}};
- bool recover = 2{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "recover"]{{end}};
- string last = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "last"]{{end}};
- uint32 since = 4{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "since"]{{end}};
- string token = 5 {{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "token"]{{end}};
+ string token = 2{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "token"]{{end}};
+ bool recover = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "recover"]{{end}};
+ uint32 seq = 4{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "seq"]{{end}};
+ uint32 gen = 5{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "gen"]{{end}};
+ string epoch = 6{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "epoch"]{{end}};
}
message SubscribeResult {
- string last = 1{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "last,omitempty"]{{end}};
- bool recovered = 2{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "recovered,omitempty"]{{end}};
- repeated Publication publications = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "publications,omitempty"]{{end}};
- bool expires = 4{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "expires,omitempty"]{{end}};
- uint32 ttl = 5{{if env.Getenv "GOGO"}} [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"]{{end}};
- uint32 time = 6{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "time,omitempty"]{{end}};
+ bool expires = 1{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "expires,omitempty"]{{end}};
+ uint32 ttl = 2{{if env.Getenv "GOGO"}} [(gogoproto.customname) = "TTL", (gogoproto.jsontag) = "ttl,omitempty"]{{end}};
+ bool recoverable = 3{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "recoverable,omitempty"]{{end}};
+ uint32 seq = 4{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "seq,omitempty"]{{end}};
+ uint32 gen = 5{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "gen,omitempty"]{{end}};
+ string epoch = 6{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "epoch,omitempty"]{{end}};
+ repeated Publication publications = 7{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "publications,omitempty"]{{end}};
+ bool recovered = 8{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "recovered,omitempty"]{{end}};
}
message SubRefreshRequest {
@@ -183,7 +187,6 @@ message PingRequest {
}
message PingResult {
- uint32 time = 1{{if env.Getenv "GOGO"}} [(gogoproto.jsontag) = "time,omitempty"]{{end}};
}
message RPCRequest{
diff --git a/node.go b/node.go
index 7ed68836..10a9d8a2 100644
--- a/node.go
+++ b/node.go
@@ -18,8 +18,6 @@ import (
"github.com/centrifugal/centrifuge/internal/proto"
"github.com/centrifugal/centrifuge/internal/proto/controlproto"
"github.com/centrifugal/centrifuge/internal/uuid"
-
- "github.com/nats-io/nuid"
)
// Node is a heart of centrifuge library – it internally keeps and manages
@@ -361,11 +359,6 @@ func (n *Node) PublishAsync(ch string, pub *Publication) <-chan error {
}
messagesSentCount.WithLabelValues("publication").Inc()
-
- if pub.UID == "" {
- pub.UID = nuid.Next()
- }
-
return n.engine.publish(ch, pub, &chOpts)
}
@@ -599,9 +592,9 @@ func (n *Node) History(ch string) ([]*Publication, error) {
}
// recoverHistory recovers publications since last UID seen by client.
-func (n *Node) recoverHistory(ch string, lastUID string) ([]*Publication, bool, error) {
+func (n *Node) recoverHistory(ch string, since recovery) ([]*Publication, bool, recovery, error) {
actionCount.WithLabelValues("recover_history").Inc()
- return n.engine.recoverHistory(ch, lastUID)
+ return n.engine.recoverHistory(ch, since)
}
// RemoveHistory removes channel history.
@@ -610,17 +603,10 @@ func (n *Node) RemoveHistory(ch string) error {
return n.engine.removeHistory(ch)
}
-// lastPublicationUID return last message id for channel.
-func (n *Node) lastPublicationUID(ch string) (string, error) {
- actionCount.WithLabelValues("last_publication_uid").Inc()
- publications, err := n.engine.history(ch, 1)
- if err != nil {
- return "", err
- }
- if len(publications) == 0 {
- return "", nil
- }
- return publications[0].UID, nil
+// lastPublicationSeq return last publication sequence and current generation for channel.
+func (n *Node) currentRecoveryData(ch string) (recovery, error) {
+ actionCount.WithLabelValues("history_recovery_data").Inc()
+ return n.engine.historyRecoveryData(ch)
}
// privateChannel checks if channel private. In case of private channel
diff --git a/node_test.go b/node_test.go
index c8535eb5..4560b5d3 100644
--- a/node_test.go
+++ b/node_test.go
@@ -87,12 +87,20 @@ func (e *TestEngine) history(ch string, limit int) ([]*proto.Publication, error)
return []*proto.Publication{}, nil
}
+func (e *TestEngine) historyLastID(ch string) (uint64, error) {
+ return 0, nil
+}
+
func (e *TestEngine) removeHistory(ch string) error {
return nil
}
-func (e *TestEngine) recoverHistory(ch string, lastUID string) ([]*proto.Publication, bool, error) {
- return []*proto.Publication{}, false, nil
+func (e *TestEngine) recoverHistory(ch string, since recovery) ([]*proto.Publication, bool, recovery, error) {
+ return []*proto.Publication{}, false, recovery{0, 0, ""}, nil
+}
+
+func (e *TestEngine) historyRecoveryData(ch string) (recovery, error) {
+ return recovery{0, 0, ""}, nil
}
func (e *TestEngine) channels() ([]string, error) {