Skip to content

Commit

Permalink
Avoid panic on concurrent writes to cached service config map (#10647)
Browse files Browse the repository at this point in the history
If multiple instances of a service are co-located on the same node then
their proxies will all share a cache entry for their resolved service
configuration. This is because the cache key contains the name of the
watched service but does not take into account the ID of the watching
proxies.

This means that there will be multiple agent service manager watches
that can wake up on the same cache update. These watchers then
concurrently modify the value in the cache when merging the resolved
config into the local proxy definitions.

To avoid this concurrent map write we will only delete the key from
opaque config in the local proxy definition after the merge, rather
than from the cached value before the merge.
  • Loading branch information
freddygv authored Jul 20, 2021
1 parent e2fff3d commit cf48218
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .changelog/10647.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
connect: fix crash that would result from multiple instances of a service resolving service config on a single agent.
```
18 changes: 9 additions & 9 deletions agent/service_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,6 @@ func mergeServiceConfig(defaults *structs.ServiceConfigResponse, service *struct
return nil, fmt.Errorf("failed to parse upstream config map for %s: %v", us.Upstream.String(), err)
}

// Delete the mesh gateway key since this is the only place it is read from an opaque map.
// Later reads use Proxy.MeshGateway.
// Note that we use the "mesh_gateway" key and not other variants like "MeshGateway" because
// UpstreamConfig.MergeInto and ResolveServiceConfig only use "mesh_gateway".
delete(us.Config, "mesh_gateway")

remoteUpstreams[us.Upstream] = structs.Upstream{
DestinationNamespace: us.Upstream.NamespaceOrDefault(),
DestinationName: us.Upstream.ID,
Expand All @@ -425,21 +419,27 @@ func mergeServiceConfig(defaults *structs.ServiceConfigResponse, service *struct
}
localUpstreams[us.DestinationID()] = struct{}{}

usCfg, ok := remoteUpstreams[us.DestinationID()]
remoteCfg, ok := remoteUpstreams[us.DestinationID()]
if !ok {
// No config defaults to merge
continue
}

// The local upstream config mode has the highest precedence, so only overwrite when it's set to the default
if us.MeshGateway.Mode == structs.MeshGatewayModeDefault {
us.MeshGateway.Mode = usCfg.MeshGateway.Mode
us.MeshGateway.Mode = remoteCfg.MeshGateway.Mode
}

// Merge in everything else that is read from the map
if err := mergo.Merge(&us.Config, usCfg.Config); err != nil {
if err := mergo.Merge(&us.Config, remoteCfg.Config); err != nil {
return nil, err
}

// Delete the mesh gateway key from opaque config since this is the value that was resolved from
// the servers and NOT the final merged value for this upstream.
// Note that we use the "mesh_gateway" key and not other variants like "MeshGateway" because
// UpstreamConfig.MergeInto and ResolveServiceConfig only use "mesh_gateway".
delete(us.Config, "mesh_gateway")
}

// Ensure upstreams present in central config are represented in the local configuration.
Expand Down
15 changes: 15 additions & 0 deletions agent/service_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package agent
import (
"encoding/json"
"fmt"
"github.com/mitchellh/copystructure"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -1188,9 +1189,16 @@ func Test_mergeServiceConfig_UpstreamOverrides(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defaultsCopy, err := copystructure.Copy(tt.args.defaults)
require.NoError(t, err)

got, err := mergeServiceConfig(tt.args.defaults, tt.args.service)
require.NoError(t, err)
assert.Equal(t, tt.want, got)

// The input defaults must not be modified by the merge.
// See PR #10647
assert.Equal(t, tt.args.defaults, defaultsCopy)
})
}
}
Expand Down Expand Up @@ -1281,9 +1289,16 @@ func Test_mergeServiceConfig_TransparentProxy(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defaultsCopy, err := copystructure.Copy(tt.args.defaults)
require.NoError(t, err)

got, err := mergeServiceConfig(tt.args.defaults, tt.args.service)
require.NoError(t, err)
assert.Equal(t, tt.want, got)

// The input defaults must not be modified by the merge.
// See PR #10647
assert.Equal(t, tt.args.defaults, defaultsCopy)
})
}
}

0 comments on commit cf48218

Please sign in to comment.