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.