github.com/anycable/anycable-go@v1.5.1/gobench/gobench.go (about) 1 // Package gobench implements alternative controller for benchmarking Go server w/o RPC. 2 // Mimics BenchmarkChannel from https://github.com/palkan/websocket-shootout/blob/master/ruby/action-cable-server/app/channels/benchmark_channel.rb 3 package gobench 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "log/slog" 9 10 "github.com/anycable/anycable-go/common" 11 "github.com/anycable/anycable-go/metrics" 12 13 nanoid "github.com/matoous/go-nanoid" 14 ) 15 16 const ( 17 metricsCalls = "gochannels_call_total" 18 ) 19 20 // Identifiers represents a connection identifiers 21 type Identifiers struct { 22 ID string `json:"id"` 23 } 24 25 // BroadcastMessage represents a pubsub payload 26 type BroadcastMessage struct { 27 Stream string `json:"stream"` 28 Data string `json:"data"` 29 } 30 31 // Controller implements node.Controller interface for gRPC 32 type Controller struct { 33 metrics *metrics.Metrics 34 log *slog.Logger 35 } 36 37 // NewController builds new Controller from config 38 func NewController(metrics *metrics.Metrics, logger *slog.Logger) *Controller { 39 metrics.RegisterCounter(metricsCalls, "The total number of Go channels calls") 40 41 return &Controller{log: logger.With("context", "gobench"), metrics: metrics} 42 } 43 44 // Start is no-op 45 func (c *Controller) Start() error { 46 return nil 47 } 48 49 // Shutdown is no-op 50 func (c *Controller) Shutdown() error { 51 return nil 52 } 53 54 // Authenticate allows everyone to connect and returns welcome message and rendom ID as identifier 55 func (c *Controller) Authenticate(sid string, env *common.SessionEnv) (*common.ConnectResult, error) { 56 c.metrics.Counter(metricsCalls).Inc() 57 58 id, err := nanoid.Nanoid() 59 60 if err != nil { 61 return nil, err 62 } 63 64 identifiers := Identifiers{ID: id} 65 idstr, err := json.Marshal(&identifiers) 66 67 if err != nil { 68 return nil, err 69 } 70 71 return &common.ConnectResult{Identifier: string(idstr), Transmissions: []string{welcomeMessage(sid)}}, nil 72 } 73 74 // Subscribe performs Command RPC call with "subscribe" command 75 func (c *Controller) Subscribe(sid string, env *common.SessionEnv, id string, channel string) (*common.CommandResult, error) { 76 c.metrics.Counter(metricsCalls).Inc() 77 res := &common.CommandResult{ 78 Disconnect: false, 79 StopAllStreams: false, 80 Streams: []string{streamFromIdentifier(channel)}, 81 Transmissions: []string{confirmationMessage(channel)}, 82 } 83 return res, nil 84 } 85 86 // Unsubscribe performs Command RPC call with "unsubscribe" command 87 func (c *Controller) Unsubscribe(sid string, env *common.SessionEnv, id string, channel string) (*common.CommandResult, error) { 88 c.metrics.Counter(metricsCalls).Inc() 89 res := &common.CommandResult{ 90 Disconnect: false, 91 StopAllStreams: true, 92 Streams: nil, 93 Transmissions: nil, 94 } 95 return res, nil 96 } 97 98 // Perform performs Command RPC call with "perform" command 99 func (c *Controller) Perform(sid string, env *common.SessionEnv, id string, channel string, data string) (res *common.CommandResult, err error) { 100 c.metrics.Counter(metricsCalls).Inc() 101 102 var payload map[string]interface{} 103 104 if err = json.Unmarshal([]byte(data), &payload); err != nil { 105 return nil, err 106 } 107 108 switch action := payload["action"].(string); action { 109 case "echo": 110 response, err := json.Marshal( 111 map[string]interface{}{ 112 "message": payload, 113 "identifier": channel, 114 }, 115 ) 116 117 if err != nil { 118 return nil, err 119 } 120 121 res = &common.CommandResult{ 122 Disconnect: false, 123 StopAllStreams: false, 124 Streams: nil, 125 Transmissions: []string{string(response)}, 126 } 127 case "broadcast": 128 broadcastMsg, err := json.Marshal(&payload) 129 130 if err != nil { 131 return nil, err 132 } 133 134 broadcast := common.StreamMessage{ 135 Stream: streamFromIdentifier(channel), 136 Data: string(broadcastMsg), 137 } 138 139 payload["action"] = "broadcastResult" 140 141 response, err := json.Marshal( 142 map[string]interface{}{ 143 "message": payload, 144 "identifier": channel, 145 }, 146 ) 147 148 if err != nil { 149 return nil, err 150 } 151 152 res = &common.CommandResult{ 153 Disconnect: false, 154 StopAllStreams: false, 155 Streams: nil, 156 Transmissions: []string{string(response)}, 157 Broadcasts: []*common.StreamMessage{&broadcast}, 158 } 159 default: 160 res = &common.CommandResult{ 161 Disconnect: false, 162 StopAllStreams: false, 163 Streams: nil, 164 Transmissions: nil, 165 } 166 } 167 168 return res, nil 169 } 170 171 // Disconnect performs disconnect RPC call 172 func (c *Controller) Disconnect(sid string, env *common.SessionEnv, id string, subscriptions []string) error { 173 c.metrics.Counter(metricsCalls).Inc() 174 return nil 175 } 176 177 func streamFromIdentifier(identifier string) string { 178 // identifier is a json of a form {"channel":"ChannelName","id":"1"} 179 // stream has a form of "all" if no "id" defined and "all#{id}" otherwise 180 var data struct { 181 Channel string `json:"channel"` 182 ID int `json:"id"` 183 } 184 185 err := json.Unmarshal([]byte(identifier), &data) 186 187 if err != nil { 188 fmt.Printf("failed to parse identifier %v: %v", identifier, err) 189 return "all" 190 } 191 192 if data.ID == 0 { 193 return "all" 194 } 195 196 return fmt.Sprintf("all%d", data.ID) 197 } 198 199 func confirmationMessage(identifier string) string { 200 data, _ := json.Marshal(struct { 201 Identifier string `json:"identifier"` 202 Type string `json:"type"` 203 }{ 204 Identifier: identifier, 205 Type: "confirm_subscription", 206 }) 207 208 return string(data) 209 } 210 211 func welcomeMessage(sid string) string { 212 return "{\"type\":\"welcome\",\"sid\":\"" + sid + "\"}" 213 }