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) {