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  }