diff --git a/pkg/server/settingswatcher/BUILD.bazel b/pkg/server/settingswatcher/BUILD.bazel index bd33266cf6b2..65e71063bcea 100644 --- a/pkg/server/settingswatcher/BUILD.bazel +++ b/pkg/server/settingswatcher/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "settingswatcher", srcs = [ + "overrides.go", "row_decoder.go", "settings_watcher.go", ], @@ -28,6 +29,7 @@ go_library( "//pkg/util/log", "//pkg/util/protoutil", "//pkg/util/stop", + "//pkg/util/syncutil", "@com_github_cockroachdb_errors//:errors", ], ) @@ -43,6 +45,7 @@ go_test( ":settingswatcher", "//pkg/base", "//pkg/keys", + "//pkg/kv/kvclient/rangefeed:with-mocks", "//pkg/roachpb:with-mocks", "//pkg/security", "//pkg/security/securitytest", @@ -56,6 +59,7 @@ go_test( "//pkg/testutils/testcluster", "//pkg/util/hlc", "//pkg/util/leaktest", + "//pkg/util/syncutil", "@com_github_cockroachdb_errors//:errors", "@com_github_stretchr_testify//require", ], diff --git a/pkg/server/settingswatcher/overrides.go b/pkg/server/settingswatcher/overrides.go new file mode 100644 index 000000000000..8986ba34b94a --- /dev/null +++ b/pkg/server/settingswatcher/overrides.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package settingswatcher + +// OverridesMonitor is an interface through which the settings watcher can +// receive setting overrides. Used for non-system tenants. +// +// The expected usage is to listen for a message on NotifyCh(), and use +// Current() to retrieve the updated list of overrides when a message is +// received. +type OverridesMonitor interface { + // NotifyCh returns a channel that receives a message any time the current set + // of overrides changes. + NotifyCh() <-chan struct{} + + // Overrides retrieves the current set of setting overrides, as a map from + // setting key to RawValue. Any settings that are present must be set to the + // overridden value. + Overrides() map[string]RawValue +} diff --git a/pkg/server/settingswatcher/row_decoder.go b/pkg/server/settingswatcher/row_decoder.go index f837e9b90ac1..427932291bc8 100644 --- a/pkg/server/settingswatcher/row_decoder.go +++ b/pkg/server/settingswatcher/row_decoder.go @@ -31,6 +31,13 @@ type RowDecoder struct { colIdxMap catalog.TableColMap } +// RawValue contains a raw-value / value-type pair, corresponding to the value +// and valueType columns of the settings table. +type RawValue struct { + Value string + Type string +} + // MakeRowDecoder makes a new RowDecoder for the settings table. func MakeRowDecoder(codec keys.SQLCodec) RowDecoder { return RowDecoder{ @@ -42,11 +49,11 @@ func MakeRowDecoder(codec keys.SQLCodec) RowDecoder { } // DecodeRow decodes a row of the system.settings table. If the value is not -// present, the setting key will be returned but the other two fields will be -// zero and the tombstone bool will be set. +// present, the setting key will be returned but the value will be zero and the +// tombstone bool will be set. func (d *RowDecoder) DecodeRow( kv roachpb.KeyValue, -) (setting, val, valType string, tombstone bool, _ error) { +) (setting string, val RawValue, tombstone bool, _ error) { tbl := systemschema.SettingsTable // First we need to decode the setting name field from the index key. { @@ -54,28 +61,28 @@ func (d *RowDecoder) DecodeRow( nameRow := make([]rowenc.EncDatum, 1) _, matches, _, err := rowenc.DecodeIndexKey(d.codec, types, nameRow, nil, kv.Key) if err != nil { - return "", "", "", false, errors.Wrap(err, "failed to decode key") + return "", RawValue{}, false, errors.Wrap(err, "failed to decode key") } if !matches { - return "", "", "", false, errors.Errorf("unexpected non-settings KV with settings prefix: %v", kv.Key) + return "", RawValue{}, false, errors.Errorf("unexpected non-settings KV with settings prefix: %v", kv.Key) } if err := nameRow[0].EnsureDecoded(types[0], &d.alloc); err != nil { - return "", "", "", false, err + return "", RawValue{}, false, err } setting = string(tree.MustBeDString(nameRow[0].Datum)) } if !kv.Value.IsPresent() { - return setting, "", "", true, nil + return setting, RawValue{}, true, nil } // The rest of the columns are stored as a family, packed with diff-encoded // column IDs followed by their values. { // column valueType can be null (missing) so we default it to "s". - valType = "s" + val.Type = "s" bytes, err := kv.Value.GetTuple() if err != nil { - return "", "", "", false, err + return "", RawValue{}, false, err } var colIDDiff uint32 var lastColID descpb.ColumnID @@ -83,28 +90,28 @@ func (d *RowDecoder) DecodeRow( for len(bytes) > 0 { _, _, colIDDiff, _, err = encoding.DecodeValueTag(bytes) if err != nil { - return "", "", "", false, err + return "", RawValue{}, false, err } colID := lastColID + descpb.ColumnID(colIDDiff) lastColID = colID if idx, ok := d.colIdxMap.Get(colID); ok { res, bytes, err = rowenc.DecodeTableValue(&d.alloc, tbl.PublicColumns()[idx].GetType(), bytes) if err != nil { - return "", "", "", false, err + return "", RawValue{}, false, err } switch colID { case tbl.PublicColumns()[1].GetID(): // value - val = string(tree.MustBeDString(res)) + val.Value = string(tree.MustBeDString(res)) case tbl.PublicColumns()[3].GetID(): // valueType - valType = string(tree.MustBeDString(res)) + val.Type = string(tree.MustBeDString(res)) case tbl.PublicColumns()[2].GetID(): // lastUpdated // TODO(dt): we could decode just the len and then seek `bytes` past // it, without allocating/decoding the unused timestamp. default: - return "", "", "", false, errors.Errorf("unknown column: %v", colID) + return "", RawValue{}, false, errors.Errorf("unknown column: %v", colID) } } } } - return setting, val, valType, false, nil + return setting, val, false, nil } diff --git a/pkg/server/settingswatcher/row_decoder_external_test.go b/pkg/server/settingswatcher/row_decoder_external_test.go index 185b7963bd2f..b718daa4b004 100644 --- a/pkg/server/settingswatcher/row_decoder_external_test.go +++ b/pkg/server/settingswatcher/row_decoder_external_test.go @@ -76,24 +76,24 @@ func TestRowDecoder(t *testing.T) { Value: *row.Value, } - k, val, valType, tombstone, err := dec.DecodeRow(kv) + k, val, tombstone, err := dec.DecodeRow(kv) require.NoError(t, err) require.False(t, tombstone) if exp, ok := toSet[k]; ok { - require.Equal(t, exp.expStr, val) - require.Equal(t, exp.expValType, valType) + require.Equal(t, exp.expStr, val.Value) + require.Equal(t, exp.expValType, val.Type) delete(toSet, k) } // Test the tombstone logic while we're here. { kv.Value.Reset() - tombstoneK, val, valType, tombstone, err := dec.DecodeRow(kv) + tombstoneK, val, tombstone, err := dec.DecodeRow(kv) require.NoError(t, err) require.True(t, tombstone) require.Equal(t, k, tombstoneK) - require.Zero(t, val) - require.Zero(t, valType) + require.Zero(t, val.Value) + require.Zero(t, val.Type) } } require.Len(t, toSet, 0) diff --git a/pkg/server/settingswatcher/settings_watcher.go b/pkg/server/settingswatcher/settings_watcher.go index 95f84bdd358d..5a24b40b2252 100644 --- a/pkg/server/settingswatcher/settings_watcher.go +++ b/pkg/server/settingswatcher/settings_watcher.go @@ -27,6 +27,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/cockroach/pkg/util/protoutil" "github.com/cockroachdb/cockroach/pkg/util/stop" + "github.com/cockroachdb/cockroach/pkg/util/syncutil" "github.com/cockroachdb/errors" ) @@ -39,6 +40,15 @@ type SettingsWatcher struct { f *rangefeed.Factory stopper *stop.Stopper dec RowDecoder + + overridesMonitor OverridesMonitor + + mu struct { + syncutil.Mutex + + values map[string]RawValue + overrides map[string]RawValue + } } // New constructs a new SettingsWatcher. @@ -59,6 +69,21 @@ func New( } } +// NewWithOverrides constructs a new SettingsWatcher which allows external +// overrides, discovered through an OverridesMonitor. +func NewWithOverrides( + clock *hlc.Clock, + codec keys.SQLCodec, + settingsToUpdate *cluster.Settings, + f *rangefeed.Factory, + stopper *stop.Stopper, + overridesMonitor OverridesMonitor, +) *SettingsWatcher { + s := New(clock, codec, settingsToUpdate, f, stopper) + s.overridesMonitor = overridesMonitor + return s +} + // Start will start the SettingsWatcher. It returns after the initial settings // have been retrieved. An error will be returned if the context is canceled or // the stopper is stopped prior to the initial data being retrieved. @@ -72,48 +97,59 @@ func (s *SettingsWatcher) Start(ctx context.Context) error { u := s.settings.MakeUpdater() initialScanDone := make(chan struct{}) var initialScanErr error + + s.mu.values = make(map[string]RawValue) + + if s.overridesMonitor != nil { + s.mu.overrides = make(map[string]RawValue) + // Initialize the overrides. We want to do this before processing the + // settings table, otherwise we could see temporary transitions to the value + // in the table. + s.updateOverrides(ctx, u) + + // Set up a worker to watch the monitor. + if err := s.stopper.RunAsyncTask(ctx, "setting-overrides", func(ctx context.Context) { + overridesCh := s.overridesMonitor.NotifyCh() + for { + select { + case <-overridesCh: + s.updateOverrides(ctx, u) + + case <-s.stopper.ShouldQuiesce(): + return + } + } + }); err != nil { + // We are shutting down. + return err + } + } + rf, err := s.f.RangeFeed(ctx, "settings", []roachpb.Span{settingsTableSpan}, now, func( ctx context.Context, kv *roachpb.RangeFeedValue, ) { - k, val, valType, tombstone, err := s.dec.DecodeRow(roachpb.KeyValue{ + setting, val, tombstone, err := s.dec.DecodeRow(roachpb.KeyValue{ Key: kv.Key, Value: kv.Value, }) if err != nil { log.Warningf(ctx, "failed to decode settings row %v: %v", kv.Key, err) + return } - // This event corresponds to a deletion. + s.mu.Lock() + defer s.mu.Unlock() + _, hasOverride := s.mu.overrides[setting] if tombstone { - s, ok := settings.Lookup(k, settings.LookupForLocalAccess) - if !ok { - log.Warningf(ctx, "failed to find setting %s, skipping update", - log.Safe(k)) - return + // This event corresponds to a deletion. + delete(s.mu.values, setting) + if !hasOverride { + s.setDefault(ctx, u, setting) } - ws, ok := s.(settings.NonMaskedSetting) - if !ok { - log.Fatalf(ctx, "expected writable setting, got %T", s) + } else { + s.mu.values[setting] = val + if !hasOverride { + s.set(ctx, u, setting, val) } - val, valType = ws.EncodedDefault(), ws.Typ() - } - - // The system tenant (i.e. the KV layer) does not use the SettingsWatcher - // to propagate cluster version changes (it uses the BumpClusterVersion - // RPC). However, non-system tenants (i.e. SQL pods) (asynchronously) get - // word of the new cluster version below. - const versionSettingKey = "version" - if k == versionSettingKey && !s.codec.ForSystemTenant() { - var v clusterversion.ClusterVersion - if err := protoutil.Unmarshal([]byte(val), &v); err != nil { - log.Warningf(ctx, "failed to set cluster version: %v", err) - } else if err := s.settings.Version.SetActiveVersion(ctx, v); err != nil { - log.Warningf(ctx, "failed to set cluster version: %v", err) - } else { - log.Infof(ctx, "set cluster version to: %v", v) - } - } else if err := u.Set(ctx, k, val, valType); err != nil { - log.Warningf(ctx, "failed to set setting %s to %s: %v", - log.Safe(k), val, err) } }, rangefeed.WithInitialScan(func(ctx context.Context) { u.ResetRemaining(ctx) @@ -134,8 +170,11 @@ func (s *SettingsWatcher) Start(ctx context.Context) error { return shouldFail })) if err != nil { + // We are shutting down return err } + + // Wait for the initial scan before returning. select { case <-initialScanDone: if initialScanErr != nil { @@ -143,11 +182,92 @@ func (s *SettingsWatcher) Start(ctx context.Context) error { } s.stopper.AddCloser(rf) return nil + case <-s.stopper.ShouldQuiesce(): - return errors.Wrap(stop.ErrUnavailable, - "failed to retrieve initial cluster settings") + return errors.Wrap(stop.ErrUnavailable, "failed to retrieve initial cluster settings") + case <-ctx.Done(): - return errors.Wrap(ctx.Err(), - "failed to retrieve initial cluster settings") + return errors.Wrap(ctx.Err(), "failed to retrieve initial cluster settings") + } +} + +const versionSettingKey = "version" + +// set the current value of a setting. +func (s *SettingsWatcher) set(ctx context.Context, u settings.Updater, key string, val RawValue) { + // The system tenant (i.e. the KV layer) does not use the SettingsWatcher + // to propagate cluster version changes (it uses the BumpClusterVersion + // RPC). However, non-system tenants (i.e. SQL pods) (asynchronously) get + // word of the new cluster version below. + if key == versionSettingKey && !s.codec.ForSystemTenant() { + var v clusterversion.ClusterVersion + if err := protoutil.Unmarshal([]byte(val.Value), &v); err != nil { + log.Warningf(ctx, "failed to set cluster version: %v", err) + } else if err := s.settings.Version.SetActiveVersion(ctx, v); err != nil { + log.Warningf(ctx, "failed to set cluster version: %v", err) + } else { + log.Infof(ctx, "set cluster version to: %v", v) + } + return + } + + if err := u.Set(ctx, key, val.Value, val.Type); err != nil { + log.Warningf(ctx, "failed to set setting %s to %s: %v", log.Safe(key), val.Value, err) + } +} + +// setDefault sets a setting to its default value. +func (s *SettingsWatcher) setDefault(ctx context.Context, u settings.Updater, key string) { + setting, ok := settings.Lookup(key, settings.LookupForLocalAccess) + if !ok { + log.Warningf(ctx, "failed to find setting %s, skipping update", log.Safe(key)) + return + } + ws, ok := setting.(settings.NonMaskedSetting) + if !ok { + log.Fatalf(ctx, "expected non-masked setting, got %T", s) + } + val := RawValue{ + Value: ws.EncodedDefault(), + Type: ws.Typ(), + } + s.set(ctx, u, key, val) +} + +// updateOverrides updates the overrides map and updates any settings +// accordingly. +func (s *SettingsWatcher) updateOverrides(ctx context.Context, u settings.Updater) { + newOverrides := s.overridesMonitor.Overrides() + + s.mu.Lock() + defer s.mu.Unlock() + + for key, val := range newOverrides { + if key == versionSettingKey { + log.Warningf(ctx, "ignoring attempt to override %s", key) + continue + } + if oldVal, hasExisting := s.mu.overrides[key]; hasExisting && oldVal == val { + // We already have the same override in place; ignore. + continue + } + // A new override was added or an existing override has changed. + s.mu.overrides[key] = val + s.set(ctx, u, key, val) + } + + // Clean up any overrides that were removed. + for key := range s.mu.overrides { + if _, ok := newOverrides[key]; !ok { + delete(s.mu.overrides, key) + + // Reset the setting to the value in the settings table (or the default + // value). + if val, ok := s.mu.values[key]; ok { + s.set(ctx, u, key, val) + } else { + s.setDefault(ctx, u, key) + } + } } } diff --git a/pkg/server/settingswatcher/settings_watcher_external_test.go b/pkg/server/settingswatcher/settings_watcher_external_test.go index 030dda42d5e1..7405f453d401 100644 --- a/pkg/server/settingswatcher/settings_watcher_external_test.go +++ b/pkg/server/settingswatcher/settings_watcher_external_test.go @@ -13,28 +13,32 @@ package settingswatcher_test import ( "context" "testing" + "time" "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/keys" + "github.com/cockroachdb/cockroach/pkg/kv/kvclient/rangefeed" "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/server/settingswatcher" "github.com/cockroachdb/cockroach/pkg/settings" "github.com/cockroachdb/cockroach/pkg/settings/cluster" "github.com/cockroachdb/cockroach/pkg/sql" "github.com/cockroachdb/cockroach/pkg/testutils" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" "github.com/cockroachdb/cockroach/pkg/util/hlc" "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/cockroach/pkg/util/syncutil" "github.com/cockroachdb/errors" "github.com/stretchr/testify/require" ) -// TestSettingsWatcher constructs a SettingsWatcher under a hypothetical tenant -// and then copies some values over to that tenant. It then ensures that the -// initial settings are picked up and that changes are also eventually picked -// up. -func TestSettingWatcher(t *testing.T) { +// TestSettingsWatcherOnTenant constructs a SettingsWatcher under a hypothetical +// tenant and then copies some values over to that tenant. It then ensures that +// the initial settings are picked up and that changes are also eventually +// picked up. +func TestSettingWatcherOnTenant(t *testing.T) { defer leaktest.AfterTest(t)() ctx := context.Background() @@ -97,3 +101,160 @@ func TestSettingWatcher(t *testing.T) { return checkSettingsValuesMatch(s0.ClusterSettings(), fakeSettings) }) } + +var _ = settings.RegisterStringSetting(settings.TenantWritable, "str.foo", "desc", "") +var _ = settings.RegisterStringSetting(settings.TenantWritable, "str.bar", "desc", "bar") +var _ = settings.RegisterIntSetting(settings.TenantWritable, "i0", "desc", 0) +var _ = settings.RegisterIntSetting(settings.TenantWritable, "i1", "desc", 1) + +func TestSettingsWatcherWithOverrides(t *testing.T) { + defer leaktest.AfterTest(t)() + + ctx := context.Background() + // Set up a test cluster for the system table. + ts, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{}) + stopper := ts.Stopper() + defer stopper.Stop(ctx) + + r := sqlutils.MakeSQLRunner(db) + // Set some settings (to verify handling of existing rows). + r.Exec(t, "SET CLUSTER SETTING str.foo = 'foo'") + r.Exec(t, "SET CLUSTER SETTING i1 = 10") + + m := newTestingOverrideMonitor() + // Set an override (to verify that it does work when it is already set). + m.set("str.foo", "override", "s") + + st := cluster.MakeTestingClusterSettings() + f, err := rangefeed.NewFactory(stopper, kvDB, st, &rangefeed.TestingKnobs{}) + require.NoError(t, err) + w := settingswatcher.NewWithOverrides(ts.Clock(), keys.SystemSQLCodec, st, f, stopper, m) + require.NoError(t, w.Start(ctx)) + + expect := func(setting, value string) { + t.Helper() + s, ok := settings.Lookup(setting, settings.LookupForLocalAccess) + require.True(t, ok) + require.Equal(t, value, s.String(&st.SV)) + } + + expectSoon := func(setting, value string) { + t.Helper() + s, ok := settings.Lookup(setting, settings.LookupForLocalAccess) + require.True(t, ok) + testutils.SucceedsSoon(t, func() error { + if actual := s.String(&st.SV); actual != value { + return errors.Errorf("expected '%s', got '%s'", value, actual) + } + return nil + }) + } + + expect("str.foo", "override") + expect("str.bar", "bar") + expect("i0", "0") + expect("i1", "10") + + m.unset("str.foo") + m.set("str.bar", "override", "s") + m.notify() + + expectSoon("str.bar", "override") + // str.foo should now be the value we set above. + expectSoon("str.foo", "foo") + + // Verify that a new setting in the table does not affect the override. + r.Exec(t, "SET CLUSTER SETTING str.bar = 'baz'") + // Sleep a bit so the settings watcher has a chance to react. + time.Sleep(time.Millisecond) + expect("str.bar", "override") + + m.set("i1", "15", "i") + m.set("i0", "20", "i") + m.notify() + expectSoon("i1", "15") + expectSoon("i0", "20") + + m.unset("str.bar") + m.notify() + expectSoon("str.bar", "baz") + + m.unset("i0") + m.unset("i1") + m.notify() + + // i0 should revert to the default. + expectSoon("i0", "0") + // i1 should revert to value in the table. + expectSoon("i1", "10") + + // Verify that version cannot be overridden. + version, ok := settings.Lookup("version", settings.LookupForLocalAccess) + require.True(t, ok) + versionValue := version.String(&st.SV) + + m.set("version", "12345", "m") + m.notify() + // Sleep a bit so the settings watcher has a chance to react. + time.Sleep(time.Millisecond) + expect("version", versionValue) +} + +// testingOverrideMonitor is a test-only implementation of OverrideMonitor. +type testingOverrideMonitor struct { + ch chan struct{} + + mu struct { + syncutil.Mutex + overrides map[string]settingswatcher.RawValue + } +} + +var _ settingswatcher.OverridesMonitor = (*testingOverrideMonitor)(nil) + +func newTestingOverrideMonitor() *testingOverrideMonitor { + m := &testingOverrideMonitor{ + ch: make(chan struct{}, 1), + } + m.mu.overrides = make(map[string]settingswatcher.RawValue) + return m +} + +func (m *testingOverrideMonitor) notify() { + select { + case m.ch <- struct{}{}: + default: + } +} + +func (m *testingOverrideMonitor) set(key string, val string, valType string) { + m.mu.Lock() + defer m.mu.Unlock() + + m.mu.overrides[key] = settingswatcher.RawValue{ + Value: val, + Type: valType, + } +} + +func (m *testingOverrideMonitor) unset(key string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.mu.overrides, key) +} + +// NotifyCh is part of the settingswatcher.OverridesMonitor interface. +func (m *testingOverrideMonitor) NotifyCh() <-chan struct{} { + return m.ch +} + +// Overrides is part of the settingswatcher.OverridesMonitor interface. +func (m *testingOverrideMonitor) Overrides() map[string]settingswatcher.RawValue { + m.mu.Lock() + defer m.mu.Unlock() + res := make(map[string]settingswatcher.RawValue) + for k, v := range m.mu.overrides { + res[k] = v + } + return res +} diff --git a/pkg/server/settingsworker.go b/pkg/server/settingsworker.go index e2d19fbbd021..0b345d646035 100644 --- a/pkg/server/settingsworker.go +++ b/pkg/server/settingsworker.go @@ -38,14 +38,14 @@ func processSystemConfigKVs( if !bytes.HasPrefix(kv.Key, settingsTablePrefix) { return nil } - k, v, t, _, err := dec.DecodeRow(kv) + k, val, _, err := dec.DecodeRow(kv) if err != nil { return err } settingsKVs = append(settingsKVs, kv) - if err := u.Set(ctx, k, v, t); err != nil { - log.Warningf(ctx, "setting %q to %q failed: %+v", k, v, err) + if err := u.Set(ctx, k, val.Value, val.Type); err != nil { + log.Warningf(ctx, "setting %q to %q failed: %+v", k, val.Value, err) } return nil } diff --git a/pkg/settings/updater.go b/pkg/settings/updater.go index 080b12a7badc..12016a41e15c 100644 --- a/pkg/settings/updater.go +++ b/pkg/settings/updater.go @@ -50,7 +50,7 @@ type updater struct { // wrapped atomic settings values as we go and note which settings were updated, // then set the rest to default in ResetRemaining(). type Updater interface { - Set(ctx context.Context, k, rawValue, valType string) error + Set(ctx context.Context, key, rawValue, valType string) error ResetRemaining(ctx context.Context) } @@ -58,7 +58,7 @@ type Updater interface { type NoopUpdater struct{} // Set implements Updater. It is a no-op. -func (u NoopUpdater) Set(ctx context.Context, k, rawValue, valType string) error { return nil } +func (u NoopUpdater) Set(ctx context.Context, key, rawValue, valType string) error { return nil } // ResetRemaining implements Updater. It is a no-op. func (u NoopUpdater) ResetRemaining(context.Context) {}