eclipse / paho.golang

Go libraries

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Client.ClientConfig public - invites race conditions

MattBrittan opened this issue · comments

Raising this issue for discussion purposes (as we are changing some API's we probably should consider others)

Is your feature request related to a problem? Please describe.

Client.ClientConfig is public so users may be tempted to make changes; for example:

c := paho.NewClient(paho.ClientConfig{
	Router: paho.NewStandardRouterWithDefault(func(m *paho.Publish) {
		log.Printf("%s : %s", m.Properties.User.Get("chatname"), string(m.Payload))
	}),
	Conn: conn,
})
// ...
ca, err := c.Connect(context.Background(), cp)
if err != nil {
	log.Fatalln(err)
}
fmt.Printf("Connected to %s\n", *server)
c.Router = paho.NewStandardRouter() // This is unsafe

This leads to race conditions; it's unsafe to change anything in the copy of ClientConfig held within Client (and mu is private so, even if that did protect the config, its not usable).

Describe the solution you'd like

I believe it probably makes more sense for the copy of ClientConfig held within Client to be private.

Describe alternatives you've considered

We could just improve the documentation but I'm not sure that there is any reason for this to be public (and it's likely to create confusion in the future).

autopaho stores it's configuration privately:

cfg       ClientConfig       // The config passed to NewConnection (stored to enable getters)

One possibility that comes to mind is the following

type ClientConfig struct {
    Router Router
    Conn   net.Conn
    // Other configuration fields
}

// NewClientConfig creates a new instance of ClientConfig with the provided settings.
// Once created, this configuration is immutable.
func NewClientConfig(router Router, conn net.Conn) ClientConfig {
    return ClientConfig{
        Router: router,
        Conn:   conn,
        // Initialize other fields as needed
    }
}

type Client struct {
    config ClientConfig
    // Other client fields
}

// NewClient creates a new Client with the given configuration.
// The configuration is copied to prevent external modifications.
func NewClient(config ClientConfig) *Client {
    return &Client{
        config: config,
        // Initialize other fields as needed
    }
}

// Usage
func main() {
    router := NewStandardRouter() // Assuming this returns a Router
    conn, err := net.Dial("tcp", "localhost:1883")
    if err != nil {
        log.Fatalf("Failed to dial: %v", err)
    }

    config := NewClientConfig(router, conn)
    client := NewClient(config)

    // The following line is not possible as ClientConfig is immutable
    // config.Router = NewOtherRouter()
}

Another approach that comes to mind

func main() {
    router := NewStandardRouter()
    conn, err := net.Dial("tcp", "localhost:1883")
    if err != nil {
        log.Fatalf("Failed to dial: %v", err)
    }

    config := NewClientConfigBuilder().
                SetRouter(router).
                SetConn(conn).
                Build()
    
    client := NewClient(config)
    // Use client...
}

@ZekeButlr the implementation of this change would be relatively simple; the question is more "should we make this change". Personally I feel that leaving ClientConfig public invites dangerous actions so it;s probably best to make it private (and I'm not sure that there is any real benefit to it being public).

The one nice thing about a public is that you can do things like this:

c := paho.NewClient(paho.ClientConfig{
		Conn: conn,
	})
c.Router = paho.NewStandardRouterWithDefault(func(m *paho.Publish) {
...
	_, err := c.Publish(context.Background(), &paho.Publish{ 
...

i.e. use the client itself in a route.

However I'm about to propose another way around that (suggesting we replace the router).

Will leave the PR sitting for a few days in case anyone wants to review/has any further thoughts on this.