github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/rpc/core/events.go (about) 1 package core 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/ari-anchor/sei-tendermint/internal/eventlog" 10 "github.com/ari-anchor/sei-tendermint/internal/eventlog/cursor" 11 "github.com/ari-anchor/sei-tendermint/internal/jsontypes" 12 tmpubsub "github.com/ari-anchor/sei-tendermint/internal/pubsub" 13 tmquery "github.com/ari-anchor/sei-tendermint/internal/pubsub/query" 14 "github.com/ari-anchor/sei-tendermint/rpc/coretypes" 15 rpctypes "github.com/ari-anchor/sei-tendermint/rpc/jsonrpc/types" 16 ) 17 18 const ( 19 // Buffer on the Tendermint (server) side to allow some slowness in clients. 20 subBufferSize = 100 21 22 // maxQueryLength is the maximum length of a query string that will be 23 // accepted. This is just a safety check to avoid outlandish queries. 24 maxQueryLength = 512 25 ) 26 27 // Subscribe for events via WebSocket. 28 // More: https://docs.tendermint.com/master/rpc/#/Websocket/subscribe 29 func (env *Environment) Subscribe(ctx context.Context, req *coretypes.RequestSubscribe) (*coretypes.ResultSubscribe, error) { 30 callInfo := rpctypes.GetCallInfo(ctx) 31 addr := callInfo.RemoteAddr() 32 33 if env.EventBus.NumClients() >= env.Config.MaxSubscriptionClients { 34 return nil, fmt.Errorf("max_subscription_clients %d reached", env.Config.MaxSubscriptionClients) 35 } else if env.EventBus.NumClientSubscriptions(addr) >= env.Config.MaxSubscriptionsPerClient { 36 return nil, fmt.Errorf("max_subscriptions_per_client %d reached", env.Config.MaxSubscriptionsPerClient) 37 } else if len(req.Query) > maxQueryLength { 38 return nil, errors.New("maximum query length exceeded") 39 } 40 41 env.Logger.Info("WARNING: Websocket subscriptions are deprecated and will be removed " + 42 "in Tendermint v0.37. See https://tinyurl.com/adr075 for more information.") 43 env.Logger.Info("Subscribe to query", "remote", addr, "query", req.Query) 44 45 q, err := tmquery.New(req.Query) 46 if err != nil { 47 return nil, fmt.Errorf("failed to parse query: %w", err) 48 } 49 50 subCtx, cancel := context.WithTimeout(ctx, SubscribeTimeout) 51 defer cancel() 52 53 sub, err := env.EventBus.SubscribeWithArgs(subCtx, tmpubsub.SubscribeArgs{ 54 ClientID: addr, 55 Query: q, 56 Limit: subBufferSize, 57 }) 58 if err != nil { 59 return nil, err 60 } 61 62 // Capture the current ID, since it can change in the future. 63 subscriptionID := callInfo.RPCRequest.ID 64 go func() { 65 opctx, opcancel := context.WithCancel(context.TODO()) 66 defer opcancel() 67 68 for { 69 msg, err := sub.Next(opctx) 70 if errors.Is(err, tmpubsub.ErrUnsubscribed) { 71 // The subscription was removed by the client. 72 return 73 } else if errors.Is(err, tmpubsub.ErrTerminated) { 74 // The subscription was terminated by the publisher. 75 resp := callInfo.RPCRequest.MakeError(err) 76 ok := callInfo.WSConn.TryWriteRPCResponse(opctx, resp) 77 if !ok { 78 env.Logger.Info("Unable to write response (slow client)", 79 "to", addr, "subscriptionID", subscriptionID, "err", err) 80 } 81 return 82 } 83 84 // We have a message to deliver to the client. 85 resp := callInfo.RPCRequest.MakeResponse(&coretypes.ResultEvent{ 86 Query: req.Query, 87 Data: msg.LegacyData(), 88 Events: msg.Events(), 89 }) 90 wctx, cancel := context.WithTimeout(opctx, 10*time.Second) 91 err = callInfo.WSConn.WriteRPCResponse(wctx, resp) 92 cancel() 93 if err != nil { 94 env.Logger.Info("Unable to write response (slow client)", 95 "to", addr, "subscriptionID", subscriptionID, "err", err) 96 } 97 } 98 }() 99 100 return &coretypes.ResultSubscribe{}, nil 101 } 102 103 // Unsubscribe from events via WebSocket. 104 // More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe 105 func (env *Environment) Unsubscribe(ctx context.Context, req *coretypes.RequestUnsubscribe) (*coretypes.ResultUnsubscribe, error) { 106 args := tmpubsub.UnsubscribeArgs{Subscriber: rpctypes.GetCallInfo(ctx).RemoteAddr()} 107 env.Logger.Info("Unsubscribe from query", "remote", args.Subscriber, "subscription", req.Query) 108 109 var err error 110 args.Query, err = tmquery.New(req.Query) 111 112 if err != nil { 113 args.ID = req.Query 114 } 115 116 err = env.EventBus.Unsubscribe(ctx, args) 117 if err != nil { 118 return nil, err 119 } 120 return &coretypes.ResultUnsubscribe{}, nil 121 } 122 123 // UnsubscribeAll from all events via WebSocket. 124 // More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe_all 125 func (env *Environment) UnsubscribeAll(ctx context.Context) (*coretypes.ResultUnsubscribe, error) { 126 addr := rpctypes.GetCallInfo(ctx).RemoteAddr() 127 env.Logger.Info("Unsubscribe from all", "remote", addr) 128 err := env.EventBus.UnsubscribeAll(ctx, addr) 129 if err != nil { 130 return nil, err 131 } 132 return &coretypes.ResultUnsubscribe{}, nil 133 } 134 135 // Events applies a query to the event log. If an event log is not enabled, 136 // Events reports an error. Otherwise, it filters the current contents of the 137 // log to return matching events. 138 // 139 // Events returns up to maxItems of the newest eligible event items. An item is 140 // eligible if it is older than before (or before is zero), it is newer than 141 // after (or after is zero), and its data matches the filter. A nil filter 142 // matches all event data. 143 // 144 // If before is zero and no eligible event items are available, Events waits 145 // for up to waitTime for a matching item to become available. The wait is 146 // terminated early if ctx ends. 147 // 148 // If maxItems ≤ 0, a default positive number of events is chosen. The values 149 // of maxItems and waitTime may be capped to sensible internal maxima without 150 // reporting an error to the caller. 151 func (env *Environment) Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error) { 152 if env.EventLog == nil { 153 return nil, errors.New("the event log is not enabled") 154 } 155 156 // Parse and validate parameters. 157 maxItems := req.MaxItems 158 if maxItems <= 0 { 159 maxItems = 10 160 } else if maxItems > 100 { 161 maxItems = 100 162 } 163 164 const minWaitTime = 1 * time.Second 165 const maxWaitTime = 30 * time.Second 166 167 waitTime := req.WaitTime 168 if waitTime < minWaitTime { 169 waitTime = minWaitTime 170 } else if waitTime > maxWaitTime { 171 waitTime = maxWaitTime 172 } 173 174 query := tmquery.All 175 if req.Filter != nil && req.Filter.Query != "" { 176 q, err := tmquery.New(req.Filter.Query) 177 if err != nil { 178 return nil, fmt.Errorf("invalid filter query: %w", err) 179 } 180 query = q 181 } 182 183 var before, after cursor.Cursor 184 if err := before.UnmarshalText([]byte(req.Before)); err != nil { 185 return nil, fmt.Errorf("invalid cursor %q: %w", req.Before, err) 186 } 187 if err := after.UnmarshalText([]byte(req.After)); err != nil { 188 return nil, fmt.Errorf("invalid cursor %q: %w", req.After, err) 189 } 190 191 var info eventlog.Info 192 var items []*eventlog.Item 193 var err error 194 accept := func(itm *eventlog.Item) error { 195 // N.B. We accept up to one item more than requested, so we can tell how 196 // to set the "more" flag in the response. 197 if len(items) > maxItems || itm.Cursor.Before(after) { 198 return eventlog.ErrStopScan 199 } 200 if cursorInRange(itm.Cursor, before, after) && query.Matches(itm.Events) { 201 items = append(items, itm) 202 } 203 return nil 204 } 205 206 if before.IsZero() { 207 ctx, cancel := context.WithTimeout(ctx, waitTime) 208 defer cancel() 209 210 // Long poll. The loop here is because new items may not match the query, 211 // and we want to keep waiting until we have relevant results (or time out). 212 cur := after 213 for len(items) == 0 { 214 info, err = env.EventLog.WaitScan(ctx, cur, accept) 215 if err != nil { 216 // Don't report a timeout as a request failure. 217 if errors.Is(err, context.DeadlineExceeded) { 218 err = nil 219 } 220 break 221 } 222 cur = info.Newest 223 } 224 } else { 225 // Quick poll, return only what is already available. 226 info, err = env.EventLog.Scan(accept) 227 } 228 if err != nil { 229 return nil, err 230 } 231 232 more := len(items) > maxItems 233 if more { 234 items = items[:len(items)-1] 235 } 236 enc, err := marshalItems(items) 237 if err != nil { 238 return nil, err 239 } 240 return &coretypes.ResultEvents{ 241 Items: enc, 242 More: more, 243 Oldest: cursorString(info.Oldest), 244 Newest: cursorString(info.Newest), 245 }, nil 246 } 247 248 func cursorString(c cursor.Cursor) string { 249 if c.IsZero() { 250 return "" 251 } 252 return c.String() 253 } 254 255 func cursorInRange(c, before, after cursor.Cursor) bool { 256 return (before.IsZero() || c.Before(before)) && (after.IsZero() || after.Before(c)) 257 } 258 259 func marshalItems(items []*eventlog.Item) ([]*coretypes.EventItem, error) { 260 out := make([]*coretypes.EventItem, len(items)) 261 for i, itm := range items { 262 v, err := jsontypes.Marshal(itm.Data) 263 if err != nil { 264 return nil, fmt.Errorf("encoding event data: %w", err) 265 } 266 out[i] = &coretypes.EventItem{Cursor: itm.Cursor.String(), Event: itm.Type} 267 out[i].Data = v 268 } 269 return out, nil 270 }