Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client.ClientConfig public - invites race conditions #210

Closed
MattBrittan opened this issue Nov 22, 2023 · 4 comments · Fixed by #221
Closed

Client.ClientConfig public - invites race conditions #210

MattBrittan opened this issue Nov 22, 2023 · 4 comments · Fixed by #221

Comments

@MattBrittan
Copy link
Contributor

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)
@ZekeButlr
Copy link

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()
}

@ZekeButlr
Copy link

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...
}

@MattBrittan
Copy link
Contributor Author

@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).

@MattBrittan
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants